aidp 0.34.1 → 0.39.3
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/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- data/lib/aidp/providers/opencode.rb +0 -139
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "timeout"
|
|
4
|
-
require_relative "base"
|
|
5
|
-
require_relative "../util"
|
|
6
|
-
require_relative "../debug_mixin"
|
|
7
|
-
|
|
8
|
-
module Aidp
|
|
9
|
-
module Providers
|
|
10
|
-
class Cursor < Base
|
|
11
|
-
include Aidp::DebugMixin
|
|
12
|
-
|
|
13
|
-
def self.available?
|
|
14
|
-
!!Aidp::Util.which("cursor-agent")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Normalize Cursor's model name to family name
|
|
18
|
-
#
|
|
19
|
-
# Cursor may use different naming conventions (e.g., dots vs hyphens)
|
|
20
|
-
#
|
|
21
|
-
# @param provider_model_name [String] Cursor's model name
|
|
22
|
-
# @return [String] The normalized family name
|
|
23
|
-
def self.model_family(provider_model_name)
|
|
24
|
-
# Normalize cursor naming to standard family names
|
|
25
|
-
# cursor uses dots: "claude-3.5-sonnet" -> "claude-3-5-sonnet"
|
|
26
|
-
provider_model_name.gsub(/(\d)\.(\d)/, '\1-\2')
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Convert family name to Cursor's naming convention
|
|
30
|
-
#
|
|
31
|
-
# @param family_name [String] The model family name
|
|
32
|
-
# @return [String] Cursor's model name
|
|
33
|
-
def self.provider_model_name(family_name)
|
|
34
|
-
# Cursor uses dots for version numbers
|
|
35
|
-
# "claude-3-5-sonnet" -> "claude-3.5-sonnet"
|
|
36
|
-
family_name.gsub(/(\d)-(\d)/, '\1.\2')
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Check if this provider supports a given model family
|
|
40
|
-
#
|
|
41
|
-
# Cursor supports Claude, GPT, and Cursor-specific models
|
|
42
|
-
#
|
|
43
|
-
# @param family_name [String] The model family name
|
|
44
|
-
# @return [Boolean] True if likely supported
|
|
45
|
-
def self.supports_model_family?(family_name)
|
|
46
|
-
family_name.match?(/^(claude|gpt|cursor)-/)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Get firewall requirements for Cursor provider
|
|
50
|
-
def self.firewall_requirements
|
|
51
|
-
{
|
|
52
|
-
domains: [
|
|
53
|
-
"cursor.com",
|
|
54
|
-
"www.cursor.com",
|
|
55
|
-
"downloads.cursor.com",
|
|
56
|
-
"api.cursor.sh",
|
|
57
|
-
"cursor.sh",
|
|
58
|
-
"app.cursor.sh",
|
|
59
|
-
"www.cursor.sh",
|
|
60
|
-
# Authentication (Auth0)
|
|
61
|
-
"auth.cursor.sh",
|
|
62
|
-
"auth0.com",
|
|
63
|
-
"*.auth0.com",
|
|
64
|
-
"a0core.net",
|
|
65
|
-
"*.a0core.net"
|
|
66
|
-
],
|
|
67
|
-
ip_ranges: []
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Discover available models from Cursor
|
|
72
|
-
#
|
|
73
|
-
# Note: Cursor doesn't have a public model listing API
|
|
74
|
-
# Returns registry-based models that match Cursor patterns
|
|
75
|
-
#
|
|
76
|
-
# @return [Array<Hash>] Array of discovered models
|
|
77
|
-
def self.discover_models
|
|
78
|
-
return [] unless available?
|
|
79
|
-
|
|
80
|
-
begin
|
|
81
|
-
require_relative "../harness/model_registry"
|
|
82
|
-
registry = Aidp::Harness::ModelRegistry.new
|
|
83
|
-
|
|
84
|
-
# Get all models from registry that Cursor might support
|
|
85
|
-
models = registry.all_families.filter_map do |family|
|
|
86
|
-
next unless supports_model_family?(family)
|
|
87
|
-
|
|
88
|
-
info = registry.get_model_info(family)
|
|
89
|
-
next unless info
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
name: provider_model_name(family),
|
|
93
|
-
family: family,
|
|
94
|
-
tier: info["tier"],
|
|
95
|
-
capabilities: info["capabilities"] || [],
|
|
96
|
-
context_window: info["context_window"],
|
|
97
|
-
provider: "cursor"
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
Aidp.log_info("cursor_provider", "using registry models", count: models.size)
|
|
102
|
-
models
|
|
103
|
-
rescue => e
|
|
104
|
-
Aidp.log_debug("cursor_provider", "discovery failed", error: e.message)
|
|
105
|
-
[]
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def name
|
|
110
|
-
"cursor"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def display_name
|
|
114
|
-
"Cursor AI"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def supports_mcp?
|
|
118
|
-
true
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def fetch_mcp_servers
|
|
122
|
-
# Try cursor-agent CLI first, then fallback to config file
|
|
123
|
-
fetch_mcp_servers_cli || fetch_mcp_servers_config
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def send_message(prompt:, session: nil, options: {})
|
|
127
|
-
raise "cursor-agent not available" unless self.class.available?
|
|
128
|
-
|
|
129
|
-
# Smart timeout calculation
|
|
130
|
-
timeout_seconds = calculate_timeout
|
|
131
|
-
|
|
132
|
-
debug_provider("cursor", "Starting execution", {timeout: timeout_seconds})
|
|
133
|
-
debug_log("📝 Sending prompt to cursor-agent (length: #{prompt.length})", level: :info)
|
|
134
|
-
|
|
135
|
-
# Set up activity monitoring
|
|
136
|
-
setup_activity_monitoring("cursor-agent", method(:activity_callback))
|
|
137
|
-
record_activity("Starting cursor-agent execution")
|
|
138
|
-
|
|
139
|
-
# Create a spinner for activity display
|
|
140
|
-
spinner = TTY::Spinner.new("[:spinner] :title", format: :dots, hide_cursor: true)
|
|
141
|
-
spinner.auto_spin
|
|
142
|
-
|
|
143
|
-
activity_display_thread = Thread.new do
|
|
144
|
-
start_time = Time.now
|
|
145
|
-
loop do
|
|
146
|
-
sleep 0.5 # Update every 500ms to reduce spam
|
|
147
|
-
elapsed = Time.now - start_time
|
|
148
|
-
|
|
149
|
-
# Break if we've been running too long or state changed
|
|
150
|
-
break if elapsed > timeout_seconds || @activity_state == :completed || @activity_state == :failed
|
|
151
|
-
|
|
152
|
-
update_spinner_status(spinner, elapsed, "🔄 cursor-agent")
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
begin
|
|
157
|
-
# Use debug_execute_command for better debugging
|
|
158
|
-
# Use -p mode (designed for non-interactive/script use)
|
|
159
|
-
# No fallback to interactive modes - they would hang AIDP's automation workflow
|
|
160
|
-
result = debug_execute_command("cursor-agent", args: ["-p"], input: prompt, timeout: timeout_seconds)
|
|
161
|
-
|
|
162
|
-
# Log the results
|
|
163
|
-
debug_command("cursor-agent", args: ["-p"], input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
164
|
-
|
|
165
|
-
if result.exit_status == 0
|
|
166
|
-
spinner.success("✓")
|
|
167
|
-
mark_completed
|
|
168
|
-
result.out
|
|
169
|
-
else
|
|
170
|
-
spinner.error("✗")
|
|
171
|
-
mark_failed("cursor-agent failed with exit code #{result.exit_status}")
|
|
172
|
-
debug_error(StandardError.new("cursor-agent failed"), {exit_code: result.exit_status, stderr: result.err})
|
|
173
|
-
raise "cursor-agent failed with exit code #{result.exit_status}: #{result.err}"
|
|
174
|
-
end
|
|
175
|
-
rescue => e
|
|
176
|
-
spinner&.error("✗")
|
|
177
|
-
mark_failed("cursor-agent execution failed: #{e.message}")
|
|
178
|
-
debug_error(e, {provider: "cursor", prompt_length: prompt.length})
|
|
179
|
-
raise
|
|
180
|
-
ensure
|
|
181
|
-
cleanup_activity_display(activity_display_thread, spinner)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
private
|
|
186
|
-
|
|
187
|
-
def activity_callback(state, message, provider)
|
|
188
|
-
# This is now handled by the animated display thread
|
|
189
|
-
# Only print static messages for state changes
|
|
190
|
-
case state
|
|
191
|
-
when :stuck
|
|
192
|
-
display_message("\n⚠️ cursor appears stuck: #{message}", type: :warning)
|
|
193
|
-
when :completed
|
|
194
|
-
display_message("\n✅ cursor completed: #{message}", type: :success)
|
|
195
|
-
when :failed
|
|
196
|
-
display_message("\n❌ cursor failed: #{message}", type: :error)
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# Try to get MCP servers via cursor-agent CLI
|
|
201
|
-
def fetch_mcp_servers_cli
|
|
202
|
-
return nil unless self.class.available?
|
|
203
|
-
|
|
204
|
-
begin
|
|
205
|
-
# Try cursor-agent mcp list (if such command exists)
|
|
206
|
-
result = debug_execute_command("cursor-agent", args: ["mcp", "list"], timeout: 5)
|
|
207
|
-
return nil unless result.exit_status == 0
|
|
208
|
-
|
|
209
|
-
parse_mcp_servers_output(result.out)
|
|
210
|
-
rescue => e
|
|
211
|
-
debug_log("Failed to fetch MCP servers via CLI: #{e.message}", level: :debug)
|
|
212
|
-
nil
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Fallback to reading Cursor's config file
|
|
217
|
-
def fetch_mcp_servers_config
|
|
218
|
-
cursor_config_path = File.expand_path("~/.cursor/mcp.json")
|
|
219
|
-
return [] unless File.exist?(cursor_config_path)
|
|
220
|
-
|
|
221
|
-
begin
|
|
222
|
-
require "json"
|
|
223
|
-
config_content = File.read(cursor_config_path)
|
|
224
|
-
config = JSON.parse(config_content)
|
|
225
|
-
|
|
226
|
-
servers = []
|
|
227
|
-
mcp_servers = config["mcpServers"] || {}
|
|
228
|
-
|
|
229
|
-
mcp_servers.each do |name, server_config|
|
|
230
|
-
# Build command description
|
|
231
|
-
command_parts = [server_config["command"]]
|
|
232
|
-
command_parts.concat(server_config["args"]) if server_config["args"]
|
|
233
|
-
command_description = command_parts.join(" ")
|
|
234
|
-
|
|
235
|
-
servers << {
|
|
236
|
-
name: name,
|
|
237
|
-
status: "configured",
|
|
238
|
-
description: command_description,
|
|
239
|
-
enabled: true,
|
|
240
|
-
source: "cursor_config",
|
|
241
|
-
env_vars: server_config["env"] || {}
|
|
242
|
-
}
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
servers
|
|
246
|
-
rescue JSON::ParserError => e
|
|
247
|
-
debug_log("Failed to parse Cursor MCP configuration: #{e.message}", level: :debug)
|
|
248
|
-
[]
|
|
249
|
-
rescue => e
|
|
250
|
-
debug_log("Failed to parse Cursor MCP configuration: #{e.message}", level: :debug)
|
|
251
|
-
[]
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Parse MCP server output from CLI commands
|
|
256
|
-
def parse_mcp_servers_output(output)
|
|
257
|
-
servers = []
|
|
258
|
-
return servers unless output
|
|
259
|
-
|
|
260
|
-
lines = output.lines
|
|
261
|
-
lines.reject! { |line| /checking mcp server health/i.match?(line) }
|
|
262
|
-
|
|
263
|
-
lines.each do |line|
|
|
264
|
-
line = line.strip
|
|
265
|
-
next if line.empty?
|
|
266
|
-
|
|
267
|
-
# Try to parse cursor-agent format: "name: status"
|
|
268
|
-
if line =~ /^([^:]+):\s*(.+)$/
|
|
269
|
-
name = Regexp.last_match(1).strip
|
|
270
|
-
status = Regexp.last_match(2).strip
|
|
271
|
-
|
|
272
|
-
servers << {
|
|
273
|
-
name: name,
|
|
274
|
-
status: status,
|
|
275
|
-
description: "MCP server managed by cursor-agent",
|
|
276
|
-
enabled: status == "ready" || status == "connected",
|
|
277
|
-
source: "cursor_cli"
|
|
278
|
-
}
|
|
279
|
-
next
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Also try to parse extended format: "name: command - ✓ Connected" (for future compatibility)
|
|
283
|
-
if line =~ /^([^:]+):\s*(.+?)\s*-\s*(✓|✗)\s*(.+)$/
|
|
284
|
-
name = Regexp.last_match(1).strip
|
|
285
|
-
command = Regexp.last_match(2).strip
|
|
286
|
-
status_symbol = Regexp.last_match(3)
|
|
287
|
-
status_text = Regexp.last_match(4).strip
|
|
288
|
-
|
|
289
|
-
servers << {
|
|
290
|
-
name: name,
|
|
291
|
-
status: (status_symbol == "✓") ? "connected" : "error",
|
|
292
|
-
description: command,
|
|
293
|
-
enabled: status_symbol == "✓",
|
|
294
|
-
error: (status_symbol == "✗") ? status_text : nil,
|
|
295
|
-
source: "cursor_cli"
|
|
296
|
-
}
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
servers
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
end
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aidp
|
|
4
|
-
module Providers
|
|
5
|
-
# ErrorTaxonomy defines the five standardized error categories that all providers
|
|
6
|
-
# use for consistent error handling, retry logic, and escalation.
|
|
7
|
-
#
|
|
8
|
-
# Categories:
|
|
9
|
-
# - rate_limited: Provider is rate-limiting requests (switch provider immediately)
|
|
10
|
-
# - auth_expired: Authentication credentials are invalid or expired (escalate or switch)
|
|
11
|
-
# - quota_exceeded: Usage quota has been exceeded (switch provider)
|
|
12
|
-
# - transient: Temporary error that may resolve on retry (retry with backoff)
|
|
13
|
-
# - permanent: Permanent error that won't resolve with retry (escalate or abort)
|
|
14
|
-
#
|
|
15
|
-
# @see https://github.com/viamin/aidp/issues/243
|
|
16
|
-
module ErrorTaxonomy
|
|
17
|
-
# Error category constants
|
|
18
|
-
RATE_LIMITED = :rate_limited
|
|
19
|
-
AUTH_EXPIRED = :auth_expired
|
|
20
|
-
QUOTA_EXCEEDED = :quota_exceeded
|
|
21
|
-
TRANSIENT = :transient
|
|
22
|
-
PERMANENT = :permanent
|
|
23
|
-
|
|
24
|
-
# All valid error categories
|
|
25
|
-
CATEGORIES = [
|
|
26
|
-
RATE_LIMITED,
|
|
27
|
-
AUTH_EXPIRED,
|
|
28
|
-
QUOTA_EXCEEDED,
|
|
29
|
-
TRANSIENT,
|
|
30
|
-
PERMANENT
|
|
31
|
-
].freeze
|
|
32
|
-
|
|
33
|
-
# Default error patterns for common error messages
|
|
34
|
-
# Providers can override these with provider-specific patterns
|
|
35
|
-
DEFAULT_PATTERNS = {
|
|
36
|
-
rate_limited: [
|
|
37
|
-
/rate.?limit/i,
|
|
38
|
-
/too.?many.?requests/i,
|
|
39
|
-
/429/,
|
|
40
|
-
/throttl(ed|ing)/i,
|
|
41
|
-
/request.?limit/i,
|
|
42
|
-
/requests.?per.?minute/i,
|
|
43
|
-
/rpm.?exceeded/i
|
|
44
|
-
],
|
|
45
|
-
auth_expired: [
|
|
46
|
-
/auth(entication|orization).?(fail(ed|ure)|error)/i,
|
|
47
|
-
/invalid.?(api.?key|token|credential)/i,
|
|
48
|
-
/expired.?(api.?key|token|credential)/i,
|
|
49
|
-
/unauthorized/i,
|
|
50
|
-
/401/,
|
|
51
|
-
/403/,
|
|
52
|
-
/permission.?denied/i,
|
|
53
|
-
/access.?denied/i
|
|
54
|
-
],
|
|
55
|
-
quota_exceeded: [
|
|
56
|
-
/quota.?(exceed(ed)?|limit|exhausted)/i,
|
|
57
|
-
/usage.?limit/i,
|
|
58
|
-
/billing.?limit/i,
|
|
59
|
-
/credit.?limit/i,
|
|
60
|
-
/insufficient.?quota/i,
|
|
61
|
-
/usage.?cap/i
|
|
62
|
-
],
|
|
63
|
-
transient: [
|
|
64
|
-
/timeout/i,
|
|
65
|
-
/timed?.?out/i,
|
|
66
|
-
/connection.?(reset|refused|lost|closed)/i,
|
|
67
|
-
/temporary.?error/i,
|
|
68
|
-
/try.?again/i,
|
|
69
|
-
/service.?unavailable/i,
|
|
70
|
-
/503/,
|
|
71
|
-
/502/,
|
|
72
|
-
/504/,
|
|
73
|
-
/gateway.?timeout/i,
|
|
74
|
-
/network.?error/i,
|
|
75
|
-
/socket.?error/i,
|
|
76
|
-
/connection.?error/i,
|
|
77
|
-
/broken.?pipe/i,
|
|
78
|
-
/host.?unreachable/i
|
|
79
|
-
],
|
|
80
|
-
permanent: [
|
|
81
|
-
/invalid.?(model|parameter|request|input)/i,
|
|
82
|
-
/unsupported.?(operation|feature|model)/i,
|
|
83
|
-
/not.?found/i,
|
|
84
|
-
/404/,
|
|
85
|
-
/bad.?request/i,
|
|
86
|
-
/400/,
|
|
87
|
-
/malformed/i,
|
|
88
|
-
/syntax.?error/i,
|
|
89
|
-
/validation.?error/i,
|
|
90
|
-
/model.?not.?available/i,
|
|
91
|
-
/model.?deprecated/i
|
|
92
|
-
]
|
|
93
|
-
}.freeze
|
|
94
|
-
|
|
95
|
-
# Retry policy for each category
|
|
96
|
-
RETRY_POLICIES = {
|
|
97
|
-
rate_limited: {
|
|
98
|
-
retry: false,
|
|
99
|
-
switch_provider: true,
|
|
100
|
-
escalate: false,
|
|
101
|
-
backoff_strategy: :none
|
|
102
|
-
},
|
|
103
|
-
auth_expired: {
|
|
104
|
-
retry: false,
|
|
105
|
-
switch_provider: true,
|
|
106
|
-
escalate: true,
|
|
107
|
-
backoff_strategy: :none
|
|
108
|
-
},
|
|
109
|
-
quota_exceeded: {
|
|
110
|
-
retry: false,
|
|
111
|
-
switch_provider: true,
|
|
112
|
-
escalate: false,
|
|
113
|
-
backoff_strategy: :none
|
|
114
|
-
},
|
|
115
|
-
transient: {
|
|
116
|
-
retry: true,
|
|
117
|
-
switch_provider: false,
|
|
118
|
-
escalate: false,
|
|
119
|
-
backoff_strategy: :exponential
|
|
120
|
-
},
|
|
121
|
-
permanent: {
|
|
122
|
-
retry: false,
|
|
123
|
-
switch_provider: false,
|
|
124
|
-
escalate: true,
|
|
125
|
-
backoff_strategy: :none
|
|
126
|
-
}
|
|
127
|
-
}.freeze
|
|
128
|
-
|
|
129
|
-
# Check if a category is valid
|
|
130
|
-
# @param category [Symbol] category to check
|
|
131
|
-
# @return [Boolean] true if valid
|
|
132
|
-
def self.valid_category?(category)
|
|
133
|
-
CATEGORIES.include?(category)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Get retry policy for a category
|
|
137
|
-
# @param category [Symbol] error category
|
|
138
|
-
# @return [Hash] retry policy configuration
|
|
139
|
-
def self.retry_policy(category)
|
|
140
|
-
RETRY_POLICIES[category] || RETRY_POLICIES[:transient]
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Classify an error message using default patterns
|
|
144
|
-
# @param message [String] error message
|
|
145
|
-
# @return [Symbol] error category
|
|
146
|
-
def self.classify_message(message)
|
|
147
|
-
return :transient if message.nil? || message.empty?
|
|
148
|
-
|
|
149
|
-
message_lower = message.downcase
|
|
150
|
-
|
|
151
|
-
# Check each category's patterns
|
|
152
|
-
DEFAULT_PATTERNS.each do |category, patterns|
|
|
153
|
-
patterns.each do |pattern|
|
|
154
|
-
return category if message_lower.match?(pattern)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Default to transient for unknown errors
|
|
159
|
-
:transient
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Check if an error category is retryable
|
|
163
|
-
# @param category [Symbol] error category
|
|
164
|
-
# @return [Boolean] true if should retry
|
|
165
|
-
def self.retryable?(category)
|
|
166
|
-
policy = retry_policy(category)
|
|
167
|
-
policy[:retry] == true
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Check if an error category should trigger provider switch
|
|
171
|
-
# @param category [Symbol] error category
|
|
172
|
-
# @return [Boolean] true if should switch provider
|
|
173
|
-
def self.should_switch_provider?(category)
|
|
174
|
-
policy = retry_policy(category)
|
|
175
|
-
policy[:switch_provider] == true
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# Check if an error category should be escalated
|
|
179
|
-
# @param category [Symbol] error category
|
|
180
|
-
# @return [Boolean] true if should escalate
|
|
181
|
-
def self.should_escalate?(category)
|
|
182
|
-
policy = retry_policy(category)
|
|
183
|
-
policy[:escalate] == true
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Get backoff strategy for a category
|
|
187
|
-
# @param category [Symbol] error category
|
|
188
|
-
# @return [Symbol] backoff strategy (:none, :linear, :exponential)
|
|
189
|
-
def self.backoff_strategy(category)
|
|
190
|
-
policy = retry_policy(category)
|
|
191
|
-
policy[:backoff_strategy] || :none
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "base"
|
|
4
|
-
require_relative "../debug_mixin"
|
|
5
|
-
|
|
6
|
-
module Aidp
|
|
7
|
-
module Providers
|
|
8
|
-
class Gemini < Base
|
|
9
|
-
include Aidp::DebugMixin
|
|
10
|
-
|
|
11
|
-
# Model name pattern for Gemini models
|
|
12
|
-
MODEL_PATTERN = /^gemini-[\d.]+-(?:pro|flash|ultra)(?:-\d+)?$/i
|
|
13
|
-
|
|
14
|
-
def self.available?
|
|
15
|
-
!!Aidp::Util.which("gemini")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Normalize a provider-specific model name to its model family
|
|
19
|
-
#
|
|
20
|
-
# Gemini may use version suffixes (e.g., "gemini-1.5-pro-001").
|
|
21
|
-
# This method strips version suffixes to get the family name.
|
|
22
|
-
#
|
|
23
|
-
# @param provider_model_name [String] The model name
|
|
24
|
-
# @return [String] The model family name
|
|
25
|
-
def self.model_family(provider_model_name)
|
|
26
|
-
# Strip version suffix: "gemini-1.5-pro-001" → "gemini-1.5-pro"
|
|
27
|
-
provider_model_name.sub(/-\d+$/, "")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Convert a model family name to the provider's preferred model name
|
|
31
|
-
#
|
|
32
|
-
# @param family_name [String] The model family name
|
|
33
|
-
# @return [String] The provider-specific model name (same as family)
|
|
34
|
-
def self.provider_model_name(family_name)
|
|
35
|
-
family_name
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Check if this provider supports a given model family
|
|
39
|
-
#
|
|
40
|
-
# @param family_name [String] The model family name
|
|
41
|
-
# @return [Boolean] True if it matches Gemini model pattern
|
|
42
|
-
def self.supports_model_family?(family_name)
|
|
43
|
-
MODEL_PATTERN.match?(family_name)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Discover available models from Gemini
|
|
47
|
-
#
|
|
48
|
-
# Note: Gemini CLI doesn't have a standard model listing command
|
|
49
|
-
# Returns registry-based models that match Gemini patterns
|
|
50
|
-
#
|
|
51
|
-
# @return [Array<Hash>] Array of discovered models
|
|
52
|
-
def self.discover_models
|
|
53
|
-
return [] unless available?
|
|
54
|
-
|
|
55
|
-
discover_models_from_registry(MODEL_PATTERN, "gemini")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Get firewall requirements for Gemini provider
|
|
59
|
-
def self.firewall_requirements
|
|
60
|
-
{
|
|
61
|
-
domains: [
|
|
62
|
-
"generativelanguage.googleapis.com",
|
|
63
|
-
"oauth2.googleapis.com",
|
|
64
|
-
"accounts.google.com",
|
|
65
|
-
"www.googleapis.com"
|
|
66
|
-
],
|
|
67
|
-
ip_ranges: []
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def name
|
|
72
|
-
"gemini"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def display_name
|
|
76
|
-
"Google Gemini"
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def send_message(prompt:, session: nil, options: {})
|
|
80
|
-
raise "gemini CLI not available" unless self.class.available?
|
|
81
|
-
|
|
82
|
-
# Smart timeout calculation
|
|
83
|
-
timeout_seconds = calculate_timeout
|
|
84
|
-
|
|
85
|
-
debug_provider("gemini", "Starting execution", {timeout: timeout_seconds})
|
|
86
|
-
debug_log("📝 Sending prompt to gemini...", level: :info)
|
|
87
|
-
|
|
88
|
-
begin
|
|
89
|
-
command_args = ["--prompt", prompt]
|
|
90
|
-
result = debug_execute_command("gemini", args: command_args, timeout: timeout_seconds)
|
|
91
|
-
|
|
92
|
-
# Log the results
|
|
93
|
-
debug_command("gemini", args: command_args, input: nil, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
94
|
-
|
|
95
|
-
if result.exit_status == 0
|
|
96
|
-
result.out
|
|
97
|
-
else
|
|
98
|
-
debug_error(StandardError.new("gemini failed"), {exit_code: result.exit_status, stderr: result.err})
|
|
99
|
-
raise "gemini failed with exit code #{result.exit_status}: #{result.err}"
|
|
100
|
-
end
|
|
101
|
-
rescue => e
|
|
102
|
-
debug_error(e, {provider: "gemini", prompt_length: prompt.length})
|
|
103
|
-
raise
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
private
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|