agent-harness 0.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 +7 -0
- data/.markdownlint.yml +6 -0
- data/.markdownlintignore +8 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +3 -0
- data/.simplecov +26 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +27 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +274 -0
- data/Rakefile +103 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/agent_harness/command_executor.rb +146 -0
- data/lib/agent_harness/configuration.rb +299 -0
- data/lib/agent_harness/error_taxonomy.rb +128 -0
- data/lib/agent_harness/errors.rb +63 -0
- data/lib/agent_harness/orchestration/circuit_breaker.rb +169 -0
- data/lib/agent_harness/orchestration/conductor.rb +179 -0
- data/lib/agent_harness/orchestration/health_monitor.rb +170 -0
- data/lib/agent_harness/orchestration/metrics.rb +167 -0
- data/lib/agent_harness/orchestration/provider_manager.rb +240 -0
- data/lib/agent_harness/orchestration/rate_limiter.rb +113 -0
- data/lib/agent_harness/providers/adapter.rb +163 -0
- data/lib/agent_harness/providers/aider.rb +109 -0
- data/lib/agent_harness/providers/anthropic.rb +345 -0
- data/lib/agent_harness/providers/base.rb +198 -0
- data/lib/agent_harness/providers/codex.rb +100 -0
- data/lib/agent_harness/providers/cursor.rb +281 -0
- data/lib/agent_harness/providers/gemini.rb +136 -0
- data/lib/agent_harness/providers/github_copilot.rb +155 -0
- data/lib/agent_harness/providers/kilocode.rb +73 -0
- data/lib/agent_harness/providers/opencode.rb +75 -0
- data/lib/agent_harness/providers/registry.rb +137 -0
- data/lib/agent_harness/response.rb +100 -0
- data/lib/agent_harness/token_tracker.rb +170 -0
- data/lib/agent_harness/version.rb +5 -0
- data/lib/agent_harness.rb +115 -0
- data/release-please-config.json +63 -0
- metadata +129 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module AgentHarness
|
|
6
|
+
module Providers
|
|
7
|
+
# Cursor AI CLI provider
|
|
8
|
+
#
|
|
9
|
+
# Provides integration with the Cursor AI coding assistant via its CLI tool.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# provider = AgentHarness::Providers::Cursor.new
|
|
13
|
+
# response = provider.send_message(prompt: "Hello!")
|
|
14
|
+
class Cursor < Base
|
|
15
|
+
class << self
|
|
16
|
+
def provider_name
|
|
17
|
+
:cursor
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def binary_name
|
|
21
|
+
"cursor-agent"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def available?
|
|
25
|
+
executor = AgentHarness.configuration.command_executor
|
|
26
|
+
!!executor.which(binary_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def firewall_requirements
|
|
30
|
+
{
|
|
31
|
+
domains: [
|
|
32
|
+
"cursor.com",
|
|
33
|
+
"www.cursor.com",
|
|
34
|
+
"downloads.cursor.com",
|
|
35
|
+
"api.cursor.sh",
|
|
36
|
+
"cursor.sh",
|
|
37
|
+
"app.cursor.sh",
|
|
38
|
+
"www.cursor.sh",
|
|
39
|
+
"auth.cursor.sh",
|
|
40
|
+
"auth0.com",
|
|
41
|
+
"*.auth0.com"
|
|
42
|
+
],
|
|
43
|
+
ip_ranges: []
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def instruction_file_paths
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
path: ".cursorrules",
|
|
51
|
+
description: "Cursor AI agent instructions",
|
|
52
|
+
symlink: true
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def discover_models
|
|
58
|
+
return [] unless available?
|
|
59
|
+
|
|
60
|
+
# Cursor doesn't have a public model listing API
|
|
61
|
+
# Return common model families it supports
|
|
62
|
+
[
|
|
63
|
+
{name: "claude-3.5-sonnet", family: "claude-3-5-sonnet", tier: "standard", provider: "cursor"},
|
|
64
|
+
{name: "claude-3.5-haiku", family: "claude-3-5-haiku", tier: "mini", provider: "cursor"},
|
|
65
|
+
{name: "gpt-4o", family: "gpt-4o", tier: "standard", provider: "cursor"},
|
|
66
|
+
{name: "cursor-small", family: "cursor-small", tier: "mini", provider: "cursor"}
|
|
67
|
+
]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Normalize Cursor's model name to family name
|
|
71
|
+
def model_family(provider_model_name)
|
|
72
|
+
# Normalize cursor naming: "claude-3.5-sonnet" -> "claude-3-5-sonnet"
|
|
73
|
+
provider_model_name.gsub(/(\d)\.(\d)/, '\1-\2')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Convert family name to Cursor's naming convention
|
|
77
|
+
def provider_model_name(family_name)
|
|
78
|
+
# Cursor uses dots: "claude-3-5-sonnet" -> "claude-3.5-sonnet"
|
|
79
|
+
family_name.gsub(/(\d)-(\d)/, '\1.\2')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if this provider supports a given model family
|
|
83
|
+
def supports_model_family?(family_name)
|
|
84
|
+
family_name.match?(/^(claude|gpt|cursor)-/)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def name
|
|
89
|
+
"cursor"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def display_name
|
|
93
|
+
"Cursor AI"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def capabilities
|
|
97
|
+
{
|
|
98
|
+
streaming: false,
|
|
99
|
+
file_upload: true,
|
|
100
|
+
vision: false,
|
|
101
|
+
tool_use: true,
|
|
102
|
+
json_mode: false,
|
|
103
|
+
mcp: true,
|
|
104
|
+
dangerous_mode: false
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def supports_mcp?
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fetch_mcp_servers
|
|
113
|
+
# Try CLI first, then config file
|
|
114
|
+
fetch_mcp_servers_cli || fetch_mcp_servers_config
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def error_patterns
|
|
118
|
+
{
|
|
119
|
+
rate_limited: [
|
|
120
|
+
/rate.?limit/i,
|
|
121
|
+
/too.?many.?requests/i,
|
|
122
|
+
/429/
|
|
123
|
+
],
|
|
124
|
+
auth_expired: [
|
|
125
|
+
/authentication.*error/i,
|
|
126
|
+
/invalid.*credentials/i,
|
|
127
|
+
/unauthorized/i
|
|
128
|
+
],
|
|
129
|
+
transient: [
|
|
130
|
+
/timeout/i,
|
|
131
|
+
/connection.*error/i,
|
|
132
|
+
/temporary/i
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Override send_message to send prompt via stdin
|
|
138
|
+
def send_message(prompt:, **options)
|
|
139
|
+
log_debug("send_message_start", prompt_length: prompt.length, options: options.keys)
|
|
140
|
+
|
|
141
|
+
# Build command (without prompt in args - we send via stdin)
|
|
142
|
+
command = [self.class.binary_name, "-p"]
|
|
143
|
+
|
|
144
|
+
# Calculate timeout
|
|
145
|
+
timeout = options[:timeout] || @config.timeout || default_timeout
|
|
146
|
+
|
|
147
|
+
# Execute command with prompt on stdin
|
|
148
|
+
start_time = Time.now
|
|
149
|
+
result = @executor.execute(command, timeout: timeout, stdin_data: prompt)
|
|
150
|
+
duration = Time.now - start_time
|
|
151
|
+
|
|
152
|
+
# Parse response
|
|
153
|
+
response = parse_response(result, duration: duration)
|
|
154
|
+
|
|
155
|
+
# Track tokens
|
|
156
|
+
track_tokens(response) if response.tokens
|
|
157
|
+
|
|
158
|
+
log_debug("send_message_complete", duration: duration)
|
|
159
|
+
|
|
160
|
+
response
|
|
161
|
+
rescue => e
|
|
162
|
+
handle_error(e, prompt: prompt, options: options)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
protected
|
|
166
|
+
|
|
167
|
+
def build_command(prompt, options)
|
|
168
|
+
# Use -p mode (designed for non-interactive/script use)
|
|
169
|
+
[self.class.binary_name, "-p"]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_env(options)
|
|
173
|
+
{}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def default_timeout
|
|
177
|
+
300
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def fetch_mcp_servers_cli
|
|
183
|
+
return nil unless self.class.available?
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
result = @executor.execute(["cursor-agent", "mcp", "list"], timeout: 5)
|
|
187
|
+
return nil unless result.success?
|
|
188
|
+
|
|
189
|
+
parse_mcp_servers_output(result.stdout)
|
|
190
|
+
rescue
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def fetch_mcp_servers_config
|
|
196
|
+
cursor_config_path = File.expand_path("~/.cursor/mcp.json")
|
|
197
|
+
return [] unless File.exist?(cursor_config_path)
|
|
198
|
+
|
|
199
|
+
begin
|
|
200
|
+
config = JSON.parse(File.read(cursor_config_path))
|
|
201
|
+
servers = []
|
|
202
|
+
mcp_servers = config["mcpServers"] || {}
|
|
203
|
+
|
|
204
|
+
mcp_servers.each do |name, server_config|
|
|
205
|
+
command_parts = [server_config["command"]]
|
|
206
|
+
command_parts.concat(server_config["args"]) if server_config["args"]
|
|
207
|
+
command_description = command_parts.join(" ")
|
|
208
|
+
|
|
209
|
+
servers << {
|
|
210
|
+
name: name,
|
|
211
|
+
status: "configured",
|
|
212
|
+
description: command_description,
|
|
213
|
+
enabled: true,
|
|
214
|
+
source: "cursor_config"
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
servers
|
|
219
|
+
rescue
|
|
220
|
+
[]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def parse_mcp_servers_output(output)
|
|
225
|
+
servers = []
|
|
226
|
+
return servers unless output
|
|
227
|
+
|
|
228
|
+
output.lines.each do |line|
|
|
229
|
+
line = line.strip
|
|
230
|
+
next if line.empty?
|
|
231
|
+
|
|
232
|
+
if line =~ /^([^:]+):\s*(.+)$/
|
|
233
|
+
name = Regexp.last_match(1).strip
|
|
234
|
+
status = Regexp.last_match(2).strip
|
|
235
|
+
|
|
236
|
+
servers << {
|
|
237
|
+
name: name,
|
|
238
|
+
status: status,
|
|
239
|
+
enabled: status == "ready" || status == "connected",
|
|
240
|
+
source: "cursor_cli"
|
|
241
|
+
}
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
servers
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def log_debug(action, **context)
|
|
249
|
+
@logger&.debug("[AgentHarness::Cursor] #{action}: #{context.inspect}")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def track_tokens(response)
|
|
253
|
+
# Cursor doesn't provide token info, so this is a no-op
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def handle_error(error, prompt:, options:)
|
|
257
|
+
classification = ErrorTaxonomy.classify(error, error_patterns)
|
|
258
|
+
|
|
259
|
+
log_error("send_message_error",
|
|
260
|
+
error: error.class.name,
|
|
261
|
+
message: error.message,
|
|
262
|
+
classification: classification)
|
|
263
|
+
|
|
264
|
+
case classification
|
|
265
|
+
when :rate_limited
|
|
266
|
+
raise RateLimitError.new(error.message, original_error: error)
|
|
267
|
+
when :auth_expired
|
|
268
|
+
raise AuthenticationError.new(error.message, original_error: error)
|
|
269
|
+
when :timeout
|
|
270
|
+
raise TimeoutError.new(error.message, original_error: error)
|
|
271
|
+
else
|
|
272
|
+
raise ProviderError.new(error.message, original_error: error)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def log_error(action, **context)
|
|
277
|
+
@logger&.error("[AgentHarness::Cursor] #{action}: #{context.inspect}")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# Google Gemini CLI provider
|
|
6
|
+
#
|
|
7
|
+
# Provides integration with the Google Gemini CLI tool.
|
|
8
|
+
class Gemini < Base
|
|
9
|
+
# Model name pattern for Gemini models
|
|
10
|
+
MODEL_PATTERN = /^gemini-[\d.]+-(?:pro|flash|ultra)(?:-\d+)?$/i
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def provider_name
|
|
14
|
+
:gemini
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def binary_name
|
|
18
|
+
"gemini"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def available?
|
|
22
|
+
executor = AgentHarness.configuration.command_executor
|
|
23
|
+
!!executor.which(binary_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def firewall_requirements
|
|
27
|
+
{
|
|
28
|
+
domains: [
|
|
29
|
+
"generativelanguage.googleapis.com",
|
|
30
|
+
"oauth2.googleapis.com",
|
|
31
|
+
"accounts.google.com",
|
|
32
|
+
"www.googleapis.com"
|
|
33
|
+
],
|
|
34
|
+
ip_ranges: []
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def instruction_file_paths
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
path: "GEMINI.md",
|
|
42
|
+
description: "Google Gemini agent instructions",
|
|
43
|
+
symlink: true
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def discover_models
|
|
49
|
+
return [] unless available?
|
|
50
|
+
|
|
51
|
+
# Gemini CLI doesn't have a standard model listing command
|
|
52
|
+
# Return common models
|
|
53
|
+
[
|
|
54
|
+
{name: "gemini-2.0-flash", family: "gemini-2-0-flash", tier: "standard", provider: "gemini"},
|
|
55
|
+
{name: "gemini-2.5-pro", family: "gemini-2-5-pro", tier: "advanced", provider: "gemini"},
|
|
56
|
+
{name: "gemini-1.5-pro", family: "gemini-1-5-pro", tier: "standard", provider: "gemini"},
|
|
57
|
+
{name: "gemini-1.5-flash", family: "gemini-1-5-flash", tier: "mini", provider: "gemini"}
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def model_family(provider_model_name)
|
|
62
|
+
# Strip version suffix: "gemini-1.5-pro-001" -> "gemini-1.5-pro"
|
|
63
|
+
provider_model_name.sub(/-\d+$/, "")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def provider_model_name(family_name)
|
|
67
|
+
family_name
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def supports_model_family?(family_name)
|
|
71
|
+
MODEL_PATTERN.match?(family_name) || family_name.start_with?("gemini-")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def name
|
|
76
|
+
"gemini"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def display_name
|
|
80
|
+
"Google Gemini"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def capabilities
|
|
84
|
+
{
|
|
85
|
+
streaming: true,
|
|
86
|
+
file_upload: true,
|
|
87
|
+
vision: true,
|
|
88
|
+
tool_use: true,
|
|
89
|
+
json_mode: true,
|
|
90
|
+
mcp: false,
|
|
91
|
+
dangerous_mode: false
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def error_patterns
|
|
96
|
+
{
|
|
97
|
+
rate_limited: [
|
|
98
|
+
/rate.?limit/i,
|
|
99
|
+
/quota.?exceeded/i,
|
|
100
|
+
/429/
|
|
101
|
+
],
|
|
102
|
+
auth_expired: [
|
|
103
|
+
/authentication/i,
|
|
104
|
+
/unauthorized/i,
|
|
105
|
+
/invalid.?credentials/i
|
|
106
|
+
],
|
|
107
|
+
transient: [
|
|
108
|
+
/timeout/i,
|
|
109
|
+
/temporary/i,
|
|
110
|
+
/503/
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
protected
|
|
116
|
+
|
|
117
|
+
def build_command(prompt, options)
|
|
118
|
+
cmd = [self.class.binary_name]
|
|
119
|
+
|
|
120
|
+
if @config.model && !@config.model.empty?
|
|
121
|
+
cmd += ["--model", @config.model]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
cmd += @config.default_flags if @config.default_flags&.any?
|
|
125
|
+
|
|
126
|
+
cmd += ["--prompt", prompt]
|
|
127
|
+
|
|
128
|
+
cmd
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_timeout
|
|
132
|
+
300
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# GitHub Copilot CLI provider
|
|
6
|
+
#
|
|
7
|
+
# Provides integration with the GitHub Copilot CLI tool.
|
|
8
|
+
class GithubCopilot < Base
|
|
9
|
+
# Model name pattern for GitHub Copilot (uses OpenAI models)
|
|
10
|
+
MODEL_PATTERN = /^gpt-[\d.o-]+(?:-turbo)?(?:-mini)?$/i
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def provider_name
|
|
14
|
+
:github_copilot
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def binary_name
|
|
18
|
+
"copilot"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def available?
|
|
22
|
+
executor = AgentHarness.configuration.command_executor
|
|
23
|
+
!!executor.which(binary_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def firewall_requirements
|
|
27
|
+
{
|
|
28
|
+
domains: [
|
|
29
|
+
"copilot-proxy.githubusercontent.com",
|
|
30
|
+
"api.githubcopilot.com",
|
|
31
|
+
"copilot-telemetry.githubusercontent.com",
|
|
32
|
+
"default.exp-tas.com",
|
|
33
|
+
"copilot-completions.githubusercontent.com"
|
|
34
|
+
],
|
|
35
|
+
ip_ranges: []
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def instruction_file_paths
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
path: ".github/copilot-instructions.md",
|
|
43
|
+
description: "GitHub Copilot agent instructions",
|
|
44
|
+
symlink: true
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def discover_models
|
|
50
|
+
return [] unless available?
|
|
51
|
+
|
|
52
|
+
[
|
|
53
|
+
{name: "gpt-4o", family: "gpt-4o", tier: "standard", provider: "github_copilot"},
|
|
54
|
+
{name: "gpt-4o-mini", family: "gpt-4o-mini", tier: "mini", provider: "github_copilot"},
|
|
55
|
+
{name: "gpt-4-turbo", family: "gpt-4-turbo", tier: "advanced", provider: "github_copilot"}
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def model_family(provider_model_name)
|
|
60
|
+
provider_model_name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def provider_model_name(family_name)
|
|
64
|
+
family_name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def supports_model_family?(family_name)
|
|
68
|
+
MODEL_PATTERN.match?(family_name)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def name
|
|
73
|
+
"github_copilot"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def display_name
|
|
77
|
+
"GitHub Copilot CLI"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def capabilities
|
|
81
|
+
{
|
|
82
|
+
streaming: false,
|
|
83
|
+
file_upload: false,
|
|
84
|
+
vision: false,
|
|
85
|
+
tool_use: true,
|
|
86
|
+
json_mode: false,
|
|
87
|
+
mcp: false,
|
|
88
|
+
dangerous_mode: true
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def supports_dangerous_mode?
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def dangerous_mode_flags
|
|
97
|
+
["--allow-all-tools"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def supports_sessions?
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def session_flags(session_id)
|
|
105
|
+
return [] unless session_id && !session_id.empty?
|
|
106
|
+
["--resume", session_id]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def error_patterns
|
|
110
|
+
{
|
|
111
|
+
auth_expired: [
|
|
112
|
+
/not.?authorized/i,
|
|
113
|
+
/access.?denied/i,
|
|
114
|
+
/permission.?denied/i,
|
|
115
|
+
/not.?enabled/i,
|
|
116
|
+
/subscription.?required/i
|
|
117
|
+
],
|
|
118
|
+
rate_limited: [
|
|
119
|
+
/usage.?limit/i,
|
|
120
|
+
/rate.?limit/i
|
|
121
|
+
],
|
|
122
|
+
transient: [
|
|
123
|
+
/connection.?error/i,
|
|
124
|
+
/timeout/i,
|
|
125
|
+
/try.?again/i
|
|
126
|
+
],
|
|
127
|
+
permanent: [
|
|
128
|
+
/invalid.?command/i,
|
|
129
|
+
/unknown.?flag/i
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
protected
|
|
135
|
+
|
|
136
|
+
def build_command(prompt, options)
|
|
137
|
+
cmd = [self.class.binary_name, "-p", prompt]
|
|
138
|
+
|
|
139
|
+
# Add dangerous mode flags by default for automation
|
|
140
|
+
cmd += dangerous_mode_flags if supports_dangerous_mode?
|
|
141
|
+
|
|
142
|
+
# Add session support if provided
|
|
143
|
+
if options[:session] && !options[:session].empty?
|
|
144
|
+
cmd += session_flags(options[:session])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
cmd
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def default_timeout
|
|
151
|
+
300
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# Kilocode CLI provider
|
|
6
|
+
#
|
|
7
|
+
# Provides integration with the Kilocode CLI tool.
|
|
8
|
+
class Kilocode < Base
|
|
9
|
+
class << self
|
|
10
|
+
def provider_name
|
|
11
|
+
:kilocode
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def binary_name
|
|
15
|
+
"kilocode"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def available?
|
|
19
|
+
executor = AgentHarness.configuration.command_executor
|
|
20
|
+
!!executor.which(binary_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def firewall_requirements
|
|
24
|
+
{
|
|
25
|
+
domains: [],
|
|
26
|
+
ip_ranges: []
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def instruction_file_paths
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def discover_models
|
|
35
|
+
return [] unless available?
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def name
|
|
41
|
+
"kilocode"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def display_name
|
|
45
|
+
"Kilocode CLI"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def capabilities
|
|
49
|
+
{
|
|
50
|
+
streaming: false,
|
|
51
|
+
file_upload: false,
|
|
52
|
+
vision: false,
|
|
53
|
+
tool_use: false,
|
|
54
|
+
json_mode: false,
|
|
55
|
+
mcp: false,
|
|
56
|
+
dangerous_mode: false
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
def build_command(prompt, options)
|
|
63
|
+
cmd = [self.class.binary_name]
|
|
64
|
+
cmd += ["--prompt", prompt]
|
|
65
|
+
cmd
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def default_timeout
|
|
69
|
+
300
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# OpenCode CLI provider
|
|
6
|
+
#
|
|
7
|
+
# Provides integration with the OpenCode CLI tool.
|
|
8
|
+
class Opencode < Base
|
|
9
|
+
class << self
|
|
10
|
+
def provider_name
|
|
11
|
+
:opencode
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def binary_name
|
|
15
|
+
"opencode"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def available?
|
|
19
|
+
executor = AgentHarness.configuration.command_executor
|
|
20
|
+
!!executor.which(binary_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def firewall_requirements
|
|
24
|
+
{
|
|
25
|
+
domains: [
|
|
26
|
+
"api.openai.com"
|
|
27
|
+
],
|
|
28
|
+
ip_ranges: []
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def instruction_file_paths
|
|
33
|
+
[]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def discover_models
|
|
37
|
+
return [] unless available?
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def name
|
|
43
|
+
"opencode"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def display_name
|
|
47
|
+
"OpenCode CLI"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def capabilities
|
|
51
|
+
{
|
|
52
|
+
streaming: false,
|
|
53
|
+
file_upload: false,
|
|
54
|
+
vision: false,
|
|
55
|
+
tool_use: false,
|
|
56
|
+
json_mode: false,
|
|
57
|
+
mcp: false,
|
|
58
|
+
dangerous_mode: false
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
protected
|
|
63
|
+
|
|
64
|
+
def build_command(prompt, options)
|
|
65
|
+
cmd = [self.class.binary_name]
|
|
66
|
+
cmd += ["--prompt", prompt]
|
|
67
|
+
cmd
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def default_timeout
|
|
71
|
+
300
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|