agent-harness 0.7.4 → 0.8.0
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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +12 -0
- data/lib/agent_harness/providers/adapter.rb +84 -0
- data/lib/agent_harness/providers/aider.rb +4 -0
- data/lib/agent_harness/providers/anthropic.rb +27 -0
- data/lib/agent_harness/providers/base.rb +56 -0
- data/lib/agent_harness/providers/codex.rb +75 -0
- data/lib/agent_harness/providers/cursor.rb +6 -0
- data/lib/agent_harness/providers/gemini.rb +66 -0
- data/lib/agent_harness/providers/github_copilot.rb +7 -0
- data/lib/agent_harness/providers/kilocode.rb +11 -0
- data/lib/agent_harness/providers/rate_limit_reset_parsing.rb +92 -0
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5bc12df7dec39319082c66c7aa4fc47bfc3f3d1765f572c4faa49677e18bad1
|
|
4
|
+
data.tar.gz: '08138f93d009f4d390917d7f09721adf7e1fbfe0eb2bedb75352b3c2250b245d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b0870b435dd46293c04eb382476138bb5b2cd1c8e6fc665a9c41fa0b4ed20be09906242a9e492f8c8f4eebd6c2da17c51ddb17860750ed634da220a54c80846
|
|
7
|
+
data.tar.gz: 1cc610554e295e6d9a65c7ac2f44209f99c45eab8b713256397a595d95c8bcd88a8a3463509dc55881b3a22701e142bb70632846b99c2f4d30a05be0888e8fd9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.4...agent-harness/v0.8.0) (2026-04-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **providers:** add config_file_content, notify_hook_content, and auth_lock_config ([#131](https://github.com/viamin/agent-harness/issues/131)) ([e95117e](https://github.com/viamin/agent-harness/commit/e95117e8000002972ca0fb31cb90dec035aa88fd))
|
|
9
|
+
* **providers:** add env var name mappings to provider classes ([#122](https://github.com/viamin/agent-harness/issues/122)) ([#133](https://github.com/viamin/agent-harness/issues/133)) ([6be9015](https://github.com/viamin/agent-harness/commit/6be901592afb02337eb2a5269f08e3025c7511c1))
|
|
10
|
+
* **providers:** add error_classification_patterns, noisy_error_patterns, and translate_error to provider classes ([#128](https://github.com/viamin/agent-harness/issues/128)) ([e2dfbed](https://github.com/viamin/agent-harness/commit/e2dfbed064fa26b2cae5691e6586e79900d19d28))
|
|
11
|
+
* **providers:** add parse_rate_limit_reset to provider base class ([#134](https://github.com/viamin/agent-harness/issues/134)) ([c16a6f8](https://github.com/viamin/agent-harness/commit/c16a6f8de312e137e5c3431f32d42b8c65126e0e))
|
|
12
|
+
* **providers:** add test_command_overrides and parse_test_error methods ([#129](https://github.com/viamin/agent-harness/issues/129)) ([a18102d](https://github.com/viamin/agent-harness/commit/a18102d1ee333a1db37f711c65e16dc20d4a0a11)), closes [#125](https://github.com/viamin/agent-harness/issues/125)
|
|
13
|
+
* **providers:** add token_usage_from_api_response to provider classes ([#130](https://github.com/viamin/agent-harness/issues/130)) ([f2c095d](https://github.com/viamin/agent-harness/commit/f2c095dcc0ae0d7822da90f98704597f08e4ed04)), closes [#126](https://github.com/viamin/agent-harness/issues/126)
|
|
14
|
+
|
|
3
15
|
## [0.7.4](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.3...agent-harness/v0.7.4) (2026-04-18)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -781,6 +781,50 @@ module AgentHarness
|
|
|
781
781
|
{}
|
|
782
782
|
end
|
|
783
783
|
|
|
784
|
+
# Error classification patterns for downstream consumers
|
|
785
|
+
#
|
|
786
|
+
# Returns patterns grouped by classification category. These patterns
|
|
787
|
+
# encode provider-specific knowledge about how each CLI reports errors
|
|
788
|
+
# and are intended for use by callers outside agent-harness.
|
|
789
|
+
#
|
|
790
|
+
# @return [Hash<Symbol, Array<Regexp>>] patterns by category
|
|
791
|
+
# (:auth_expired, :abort, :authentication, :quota)
|
|
792
|
+
def error_classification_patterns
|
|
793
|
+
{
|
|
794
|
+
auth_expired: [],
|
|
795
|
+
abort: [],
|
|
796
|
+
authentication: [],
|
|
797
|
+
quota: [
|
|
798
|
+
/requires more credits/i,
|
|
799
|
+
/insufficient credits/i,
|
|
800
|
+
/credit.*exceeded/i,
|
|
801
|
+
/spend limit.*reached/i,
|
|
802
|
+
/billing.*limit/i
|
|
803
|
+
]
|
|
804
|
+
}
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Patterns matching noisy/non-actionable error output
|
|
808
|
+
#
|
|
809
|
+
# Downstream consumers can use these to filter out log noise
|
|
810
|
+
# from provider stderr/stdout that is not meaningful for users.
|
|
811
|
+
#
|
|
812
|
+
# @return [Array<Regexp>] noisy error patterns
|
|
813
|
+
def noisy_error_patterns
|
|
814
|
+
[]
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# Translate a raw error message into a user-friendly string
|
|
818
|
+
#
|
|
819
|
+
# Providers override this to map CLI-specific error output
|
|
820
|
+
# into concise, actionable messages.
|
|
821
|
+
#
|
|
822
|
+
# @param message [String] raw error message
|
|
823
|
+
# @return [String] translated message (or the original if no match)
|
|
824
|
+
def translate_error(message)
|
|
825
|
+
message
|
|
826
|
+
end
|
|
827
|
+
|
|
784
828
|
# Authentication type for this provider
|
|
785
829
|
#
|
|
786
830
|
# @return [Symbol] :oauth for token-based auth that can expire,
|
|
@@ -902,6 +946,17 @@ module AgentHarness
|
|
|
902
946
|
[]
|
|
903
947
|
end
|
|
904
948
|
|
|
949
|
+
# Extract token usage from an API response body
|
|
950
|
+
#
|
|
951
|
+
# Parses the provider-specific API response shape and returns
|
|
952
|
+
# normalized token counts.
|
|
953
|
+
#
|
|
954
|
+
# @param body [Hash] the parsed JSON response body from the provider API
|
|
955
|
+
# @return [Hash] with :input_tokens and :output_tokens keys, or empty hash
|
|
956
|
+
def token_usage_from_api_response(body)
|
|
957
|
+
{}
|
|
958
|
+
end
|
|
959
|
+
|
|
905
960
|
# Whether this provider can extract token usage from CLI output
|
|
906
961
|
#
|
|
907
962
|
# @return [Boolean] true if the provider returns token counts
|
|
@@ -1020,6 +1075,35 @@ module AgentHarness
|
|
|
1020
1075
|
nil
|
|
1021
1076
|
end
|
|
1022
1077
|
|
|
1078
|
+
# Generate provider-specific config file content.
|
|
1079
|
+
#
|
|
1080
|
+
# Providers that require a config file written before CLI execution
|
|
1081
|
+
# (e.g. Codex TOML, Kilocode JSON) should override this method.
|
|
1082
|
+
#
|
|
1083
|
+
# @param options [Hash] provider-specific options for config generation
|
|
1084
|
+
# @return [String, nil] config file content, or nil when no config is needed
|
|
1085
|
+
def config_file_content(options = {})
|
|
1086
|
+
nil
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
# Generate provider-specific notification hook content.
|
|
1090
|
+
#
|
|
1091
|
+
# Providers that support notification hooks appended to their config
|
|
1092
|
+
# file should override this method.
|
|
1093
|
+
#
|
|
1094
|
+
# @return [String, nil] notify hook content, or nil when not applicable
|
|
1095
|
+
def notify_hook_content
|
|
1096
|
+
nil
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
# Auth lock configuration for providers that need file-based lock
|
|
1100
|
+
# serialization (e.g. OAuth refresh token coordination).
|
|
1101
|
+
#
|
|
1102
|
+
# @return [Hash, nil] lock config with :path and :timeout keys, or nil
|
|
1103
|
+
def auth_lock_config
|
|
1104
|
+
nil
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1023
1107
|
private
|
|
1024
1108
|
|
|
1025
1109
|
def classify_smoke_test_message(message)
|
|
@@ -170,6 +170,10 @@ module AgentHarness
|
|
|
170
170
|
}
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
def api_key_env_var_names = ["ANTHROPIC_API_KEY"]
|
|
174
|
+
|
|
175
|
+
def api_key_unset_vars = ["ANTHROPIC_BASE_URL", "ANTHROPIC_HEADER_X_AGENT_RUN_ID", "ANTHROPIC_HEADER_X_PROXY_TOKEN"]
|
|
176
|
+
|
|
173
177
|
def error_patterns
|
|
174
178
|
COMMON_ERROR_PATTERNS.merge(
|
|
175
179
|
auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [/incorrect.*api.*key/i],
|
|
@@ -14,6 +14,8 @@ module AgentHarness
|
|
|
14
14
|
# provider = AgentHarness::Providers::Anthropic.new
|
|
15
15
|
# response = provider.send_message(prompt: "Hello!")
|
|
16
16
|
class Anthropic < Base
|
|
17
|
+
include RateLimitResetParsing
|
|
18
|
+
|
|
17
19
|
# Model name pattern for Anthropic Claude models
|
|
18
20
|
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
19
21
|
SUPPORTED_CLI_VERSION = "2.1.92"
|
|
@@ -379,6 +381,12 @@ module AgentHarness
|
|
|
379
381
|
cleanup_mcp_tempfiles!
|
|
380
382
|
end
|
|
381
383
|
|
|
384
|
+
def api_key_env_var_names = ["ANTHROPIC_API_KEY"]
|
|
385
|
+
|
|
386
|
+
def api_key_unset_vars = ["ANTHROPIC_BASE_URL", "ANTHROPIC_HEADER_X_AGENT_RUN_ID", "ANTHROPIC_HEADER_X_PROXY_TOKEN"]
|
|
387
|
+
|
|
388
|
+
def subscription_unset_vars = ["ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"] + api_key_unset_vars
|
|
389
|
+
|
|
382
390
|
def supports_mcp?
|
|
383
391
|
true
|
|
384
392
|
end
|
|
@@ -414,6 +422,16 @@ module AgentHarness
|
|
|
414
422
|
true
|
|
415
423
|
end
|
|
416
424
|
|
|
425
|
+
def token_usage_from_api_response(body)
|
|
426
|
+
usage = body&.dig("usage")
|
|
427
|
+
return {} unless usage
|
|
428
|
+
|
|
429
|
+
{
|
|
430
|
+
input_tokens: usage["input_tokens"].to_i,
|
|
431
|
+
output_tokens: usage["output_tokens"].to_i
|
|
432
|
+
}
|
|
433
|
+
end
|
|
434
|
+
|
|
417
435
|
def execution_semantics
|
|
418
436
|
{
|
|
419
437
|
prompt_delivery: :arg,
|
|
@@ -474,6 +492,15 @@ module AgentHarness
|
|
|
474
492
|
}
|
|
475
493
|
end
|
|
476
494
|
|
|
495
|
+
def error_classification_patterns
|
|
496
|
+
super.merge(
|
|
497
|
+
abort: [
|
|
498
|
+
/free tier limit reached/i,
|
|
499
|
+
/please upgrade to a paid plan/i
|
|
500
|
+
]
|
|
501
|
+
)
|
|
502
|
+
end
|
|
503
|
+
|
|
477
504
|
def fetch_mcp_servers
|
|
478
505
|
return [] unless self.class.available?
|
|
479
506
|
|
|
@@ -205,6 +205,62 @@ module AgentHarness
|
|
|
205
205
|
@executor.is_a?(DockerCommandExecutor)
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
# Environment variable names that the provider's CLI reads for API key authentication.
|
|
209
|
+
#
|
|
210
|
+
# @return [Array<String>] env var names (empty by default)
|
|
211
|
+
def api_key_env_var_names = []
|
|
212
|
+
|
|
213
|
+
# Environment variable names to unset when the caller supplies its own API key,
|
|
214
|
+
# preventing the CLI from reading stale or conflicting proxy/header variables.
|
|
215
|
+
#
|
|
216
|
+
# @return [Array<String>] env var names (empty by default)
|
|
217
|
+
def api_key_unset_vars = []
|
|
218
|
+
|
|
219
|
+
# Environment variable names to unset when the caller uses subscription-based auth,
|
|
220
|
+
# ensuring the CLI does not pick up API-key or proxy variables that would conflict.
|
|
221
|
+
#
|
|
222
|
+
# @return [Array<String>] env var names (empty by default)
|
|
223
|
+
def subscription_unset_vars = []
|
|
224
|
+
|
|
225
|
+
# Provider-specific environment variable overrides that the caller should set
|
|
226
|
+
# when invoking the CLI (e.g. feature flags or sandbox controls).
|
|
227
|
+
#
|
|
228
|
+
# @return [Hash{String => String}] env var name => value (empty by default)
|
|
229
|
+
def cli_env_overrides = {}
|
|
230
|
+
|
|
231
|
+
# Additional CLI flags for health-check/test invocations
|
|
232
|
+
#
|
|
233
|
+
# Providers override this to supply flags that should be appended
|
|
234
|
+
# when the CLI is invoked in a test or smoke-test context.
|
|
235
|
+
#
|
|
236
|
+
# @return [Array<String>] extra CLI flags (empty by default)
|
|
237
|
+
def test_command_overrides
|
|
238
|
+
[]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Parse provider-specific error information from test output
|
|
242
|
+
#
|
|
243
|
+
# Providers override this to extract structured error details from
|
|
244
|
+
# CLI output or sidecar files produced during a test invocation.
|
|
245
|
+
#
|
|
246
|
+
# @param output [String] the CLI stdout/stderr output
|
|
247
|
+
# @param files [Hash] mapping of logical names to file paths
|
|
248
|
+
# @return [Hash, nil] structured error hash or nil if no error detected
|
|
249
|
+
def parse_test_error(output:, files: {})
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Parse rate-limit reset time from provider error output.
|
|
254
|
+
#
|
|
255
|
+
# Providers that emit rate-limit reset times should override this
|
|
256
|
+
# method (or include RateLimitResetParsing for the common format).
|
|
257
|
+
#
|
|
258
|
+
# @param text [String, nil] error output text
|
|
259
|
+
# @return [Time, nil] UTC reset time, or nil if not parseable
|
|
260
|
+
def parse_rate_limit_reset(text)
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
|
|
208
264
|
protected
|
|
209
265
|
|
|
210
266
|
# Build CLI command - override in subclasses
|
|
@@ -8,6 +8,8 @@ module AgentHarness
|
|
|
8
8
|
#
|
|
9
9
|
# Provides integration with the OpenAI Codex CLI tool.
|
|
10
10
|
class Codex < Base
|
|
11
|
+
include RateLimitResetParsing
|
|
12
|
+
|
|
11
13
|
SUPPORTED_CLI_VERSION = "0.116.0"
|
|
12
14
|
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.117.0").freeze
|
|
13
15
|
OAUTH_REFRESH_FAILURE_PATTERNS = [
|
|
@@ -167,10 +169,32 @@ module AgentHarness
|
|
|
167
169
|
}
|
|
168
170
|
end
|
|
169
171
|
|
|
172
|
+
def api_key_env_var_names = ["OPENAI_API_KEY"]
|
|
173
|
+
|
|
174
|
+
def api_key_unset_vars = ["OPENAI_BASE_URL", "OPENAI_HEADER_X_AGENT_RUN_ID", "OPENAI_HEADER_X_PROXY_TOKEN"]
|
|
175
|
+
|
|
176
|
+
def subscription_unset_vars = ["OPENAI_API_KEY", "OPENAI_BASE_URL"] + api_key_unset_vars
|
|
177
|
+
|
|
178
|
+
def cli_env_overrides = {"PAID_CODEX_SUBSCRIPTION_AUTH" => "1"}
|
|
179
|
+
|
|
180
|
+
def test_command_overrides
|
|
181
|
+
["--skip-git-repo-check", "--output-last-message"]
|
|
182
|
+
end
|
|
183
|
+
|
|
170
184
|
def dangerous_mode_flags
|
|
171
185
|
["--full-auto"]
|
|
172
186
|
end
|
|
173
187
|
|
|
188
|
+
def token_usage_from_api_response(body)
|
|
189
|
+
usage = body&.dig("usage")
|
|
190
|
+
return {} unless usage
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
input_tokens: usage["prompt_tokens"].to_i,
|
|
194
|
+
output_tokens: usage["completion_tokens"].to_i
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
174
198
|
def execution_semantics
|
|
175
199
|
{
|
|
176
200
|
prompt_delivery: :arg,
|
|
@@ -216,6 +240,31 @@ module AgentHarness
|
|
|
216
240
|
}
|
|
217
241
|
end
|
|
218
242
|
|
|
243
|
+
def error_classification_patterns
|
|
244
|
+
super.merge(
|
|
245
|
+
auth_expired: [
|
|
246
|
+
/refresh_token_reused/i,
|
|
247
|
+
/refresh token has already been used/i,
|
|
248
|
+
/Please log out and sign in again/i,
|
|
249
|
+
/authentication_error/i,
|
|
250
|
+
/invalid_grant/i,
|
|
251
|
+
/Token is expired or invalid/i
|
|
252
|
+
],
|
|
253
|
+
abort: [
|
|
254
|
+
/free tier limit reached/i,
|
|
255
|
+
/please upgrade to a paid plan/i
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def translate_error(message)
|
|
261
|
+
case message
|
|
262
|
+
when /refresh_token_reused/i then "Codex authentication expired. Please re-authenticate."
|
|
263
|
+
when /free tier limit/i then "Codex free tier limit reached."
|
|
264
|
+
else message
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
219
268
|
def auth_status
|
|
220
269
|
api_key = ENV["OPENAI_API_KEY"]
|
|
221
270
|
if api_key && !api_key.strip.empty?
|
|
@@ -272,6 +321,28 @@ module AgentHarness
|
|
|
272
321
|
{valid: errors.empty?, errors: errors}
|
|
273
322
|
end
|
|
274
323
|
|
|
324
|
+
def config_file_content(options = {})
|
|
325
|
+
<<~TOML
|
|
326
|
+
[chatgpt]
|
|
327
|
+
model_provider = "#{escape_toml_string(options[:model_provider])}"
|
|
328
|
+
base_url = "#{escape_toml_string(options[:base_url])}"
|
|
329
|
+
env_key = "#{escape_toml_string(options[:env_key])}"
|
|
330
|
+
wire_api = "#{escape_toml_string(options[:wire_api])}"
|
|
331
|
+
TOML
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def notify_hook_content
|
|
335
|
+
<<~TOML
|
|
336
|
+
|
|
337
|
+
[notify]
|
|
338
|
+
# Paid notification hook
|
|
339
|
+
TOML
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def auth_lock_config
|
|
343
|
+
{path: "/tmp/codex-auth.lock", timeout: 30}
|
|
344
|
+
end
|
|
345
|
+
|
|
275
346
|
protected
|
|
276
347
|
|
|
277
348
|
def parse_response(result, duration:)
|
|
@@ -387,6 +458,10 @@ module AgentHarness
|
|
|
387
458
|
|
|
388
459
|
private
|
|
389
460
|
|
|
461
|
+
def escape_toml_string(val)
|
|
462
|
+
val.to_s.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("\n") { "\\n" }
|
|
463
|
+
end
|
|
464
|
+
|
|
390
465
|
def parse_jsonl_output(raw_output)
|
|
391
466
|
return nil if raw_output.nil? || raw_output.strip.empty?
|
|
392
467
|
|
|
@@ -12,6 +12,8 @@ module AgentHarness
|
|
|
12
12
|
# provider = AgentHarness::Providers::Cursor.new
|
|
13
13
|
# response = provider.send_message(prompt: "Hello!")
|
|
14
14
|
class Cursor < Base
|
|
15
|
+
include RateLimitResetParsing
|
|
16
|
+
|
|
15
17
|
INSTALL_SCRIPT_URL = "https://cursor.com/install"
|
|
16
18
|
INSTALL_TARGET_LATEST = "latest"
|
|
17
19
|
INSTALL_BUILD = "2026.03.30-a5d3e17"
|
|
@@ -206,6 +208,10 @@ module AgentHarness
|
|
|
206
208
|
fetch_mcp_servers_cli || fetch_mcp_servers_config
|
|
207
209
|
end
|
|
208
210
|
|
|
211
|
+
def api_key_env_var_names = ["ANTHROPIC_API_KEY"]
|
|
212
|
+
|
|
213
|
+
def api_key_unset_vars = ["ANTHROPIC_BASE_URL", "ANTHROPIC_HEADER_X_AGENT_RUN_ID", "ANTHROPIC_HEADER_X_PROXY_TOKEN"]
|
|
214
|
+
|
|
209
215
|
def auth_type
|
|
210
216
|
:oauth
|
|
211
217
|
end
|
|
@@ -163,10 +163,47 @@ module AgentHarness
|
|
|
163
163
|
}
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
def parse_test_error(output:, files: {})
|
|
167
|
+
error_file = files.values.find { |path| path.match?(/gemini-client-error-.*\.json/) }
|
|
168
|
+
return nil unless error_file
|
|
169
|
+
|
|
170
|
+
error_data = begin
|
|
171
|
+
JSON.parse(File.read(error_file))
|
|
172
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
return nil unless error_data
|
|
176
|
+
|
|
177
|
+
{message: error_data.dig("error", "message") || output, type: :configuration}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def api_key_env_var_names = ["GEMINI_API_KEY", "GOOGLE_API_KEY"]
|
|
181
|
+
|
|
182
|
+
def api_key_unset_vars = ["GOOGLE_GEMINI_BASE_URL", "GOOGLE_GENAI_BASE_URL", "GEMINI_CLI_CUSTOM_HEADERS"]
|
|
183
|
+
|
|
184
|
+
def subscription_unset_vars = ["GEMINI_API_KEY", "GOOGLE_GEMINI_BASE_URL", "GOOGLE_GENAI_BASE_URL", "GEMINI_CLI_CUSTOM_HEADERS"]
|
|
185
|
+
|
|
186
|
+
def cli_env_overrides
|
|
187
|
+
{
|
|
188
|
+
"GEMINI_SANDBOX" => "false",
|
|
189
|
+
"GEMINI_CLI_DISABLE_RETRIES" => "true"
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
166
193
|
def auth_type
|
|
167
194
|
:oauth
|
|
168
195
|
end
|
|
169
196
|
|
|
197
|
+
def token_usage_from_api_response(body)
|
|
198
|
+
usage = body&.dig("usageMetadata")
|
|
199
|
+
return {} unless usage
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
input_tokens: usage["promptTokenCount"].to_i,
|
|
203
|
+
output_tokens: usage["candidatesTokenCount"].to_i
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
|
|
170
207
|
def execution_semantics
|
|
171
208
|
{
|
|
172
209
|
prompt_delivery: :flag,
|
|
@@ -204,6 +241,35 @@ module AgentHarness
|
|
|
204
241
|
}
|
|
205
242
|
end
|
|
206
243
|
|
|
244
|
+
def error_classification_patterns
|
|
245
|
+
super.merge(
|
|
246
|
+
authentication: [
|
|
247
|
+
/GEMINI_API_KEY/i,
|
|
248
|
+
/GOOGLE_GENAI_USE_VERTEXAI/i,
|
|
249
|
+
/GOOGLE_GENAI_USE_GCA/i,
|
|
250
|
+
/ValidationRequiredError/i,
|
|
251
|
+
/API key not configured for google/i,
|
|
252
|
+
/API key not valid/i
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def noisy_error_patterns
|
|
258
|
+
[
|
|
259
|
+
/Error when talking to Gemini API/i,
|
|
260
|
+
/service=.*status/i,
|
|
261
|
+
/loading\.\.\./i,
|
|
262
|
+
/subscribing/i
|
|
263
|
+
]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def translate_error(message)
|
|
267
|
+
case message
|
|
268
|
+
when /API key not configured/i then "Gemini API key not set. Run: export GEMINI_API_KEY=..."
|
|
269
|
+
else message
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
207
273
|
def auth_status
|
|
208
274
|
api_key = [ENV["GEMINI_API_KEY"], ENV["GOOGLE_API_KEY"]].find { |key| key && !key.strip.empty? }
|
|
209
275
|
if api_key
|
|
@@ -191,6 +191,13 @@ module AgentHarness
|
|
|
191
191
|
}
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
def translate_error(message)
|
|
195
|
+
case message
|
|
196
|
+
when /github-copilot-cli.*not found/i then "GitHub Copilot CLI not installed."
|
|
197
|
+
else message
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
194
201
|
def supports_token_counting?
|
|
195
202
|
supports_json_output_format?
|
|
196
203
|
end
|
|
@@ -113,6 +113,10 @@ module AgentHarness
|
|
|
113
113
|
"Kilocode CLI"
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
def test_command_overrides
|
|
117
|
+
["--auto", "--print-logs"]
|
|
118
|
+
end
|
|
119
|
+
|
|
116
120
|
def capabilities
|
|
117
121
|
{
|
|
118
122
|
streaming: false,
|
|
@@ -125,6 +129,13 @@ module AgentHarness
|
|
|
125
129
|
}
|
|
126
130
|
end
|
|
127
131
|
|
|
132
|
+
def config_file_content(options = {})
|
|
133
|
+
{
|
|
134
|
+
provider: options[:api_provider],
|
|
135
|
+
model: options[:model_id]
|
|
136
|
+
}.to_json
|
|
137
|
+
end
|
|
138
|
+
|
|
128
139
|
def error_patterns
|
|
129
140
|
COMMON_ERROR_PATTERNS
|
|
130
141
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# Shared rate-limit reset time parsing for providers whose CLIs emit
|
|
6
|
+
# standard reset-time formats in error output.
|
|
7
|
+
#
|
|
8
|
+
# Include this module in any provider that uses the common format:
|
|
9
|
+
# - "retry after 60s" (seconds)
|
|
10
|
+
# - "reset at 1234567890" (unix timestamp)
|
|
11
|
+
# - "resets 5am (UTC)" (time today/tomorrow)
|
|
12
|
+
# - "resets 5:00am (UTC)" (time with minutes)
|
|
13
|
+
# - "resets Jan 15, 5pm (UTC)" (date + time)
|
|
14
|
+
#
|
|
15
|
+
# Providers with a different format should override
|
|
16
|
+
# +parse_rate_limit_reset+ directly instead of including this module.
|
|
17
|
+
module RateLimitResetParsing
|
|
18
|
+
# Parse rate-limit reset time from provider error output.
|
|
19
|
+
#
|
|
20
|
+
# @param text [String, nil] error output text
|
|
21
|
+
# @return [Time, nil] UTC reset time, or nil if not parseable
|
|
22
|
+
def parse_rate_limit_reset(text)
|
|
23
|
+
return nil unless text
|
|
24
|
+
|
|
25
|
+
parse_retry_after(text) ||
|
|
26
|
+
parse_reset_at(text) ||
|
|
27
|
+
parse_resets_time(text) ||
|
|
28
|
+
parse_resets_date_time(text)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# "retry after 60s"
|
|
34
|
+
def parse_retry_after(text)
|
|
35
|
+
match = text.match(/retry\s+after\s+(\d+)\s*s/i)
|
|
36
|
+
return unless match
|
|
37
|
+
|
|
38
|
+
Time.now.utc + match[1].to_i
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# "reset at 1234567890" (unix timestamp)
|
|
42
|
+
def parse_reset_at(text)
|
|
43
|
+
match = text.match(/reset\s+at\s+(\d{10})/i)
|
|
44
|
+
return unless match
|
|
45
|
+
|
|
46
|
+
Time.at(match[1].to_i).utc
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# "resets 5am (UTC)" / "resets 5:00am (UTC)"
|
|
50
|
+
def parse_resets_time(text)
|
|
51
|
+
match = text.match(/resets\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i)
|
|
52
|
+
return unless match
|
|
53
|
+
|
|
54
|
+
hour = match[1].to_i
|
|
55
|
+
minute = match[2]&.to_i || 0
|
|
56
|
+
meridiem = match[3].downcase
|
|
57
|
+
hour = to_24h(hour, meridiem)
|
|
58
|
+
|
|
59
|
+
now = Time.now.utc
|
|
60
|
+
reset_time = Time.utc(now.year, now.month, now.day, hour, minute)
|
|
61
|
+
reset_time += 86_400 if reset_time <= now
|
|
62
|
+
reset_time
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# "resets Jan 15, 5pm (UTC)"
|
|
66
|
+
def parse_resets_date_time(text)
|
|
67
|
+
match = text.match(/resets\s+(\w{3})\s+(\d{1,2}),?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i)
|
|
68
|
+
return unless match
|
|
69
|
+
|
|
70
|
+
month = Date::ABBR_MONTHNAMES.index(match[1].capitalize)
|
|
71
|
+
return unless month
|
|
72
|
+
|
|
73
|
+
day = match[2].to_i
|
|
74
|
+
hour = match[3].to_i
|
|
75
|
+
minute = match[4]&.to_i || 0
|
|
76
|
+
meridiem = match[5].downcase
|
|
77
|
+
hour = to_24h(hour, meridiem)
|
|
78
|
+
|
|
79
|
+
year = Time.now.utc.year
|
|
80
|
+
year += 1 if month < Time.now.utc.month
|
|
81
|
+
|
|
82
|
+
Time.utc(year, month, day, hour, minute)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_24h(hour, meridiem)
|
|
86
|
+
hour += 12 if meridiem == "pm" && hour != 12
|
|
87
|
+
hour = 0 if meridiem == "am" && hour == 12
|
|
88
|
+
hour
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/agent_harness.rb
CHANGED
|
@@ -254,6 +254,7 @@ require_relative "agent_harness/providers/registry"
|
|
|
254
254
|
require_relative "agent_harness/providers/adapter"
|
|
255
255
|
require_relative "agent_harness/providers/base"
|
|
256
256
|
require_relative "agent_harness/providers/token_usage_parsing"
|
|
257
|
+
require_relative "agent_harness/providers/rate_limit_reset_parsing"
|
|
257
258
|
require_relative "agent_harness/providers/anthropic"
|
|
258
259
|
require_relative "agent_harness/providers/aider"
|
|
259
260
|
require_relative "agent_harness/providers/codex"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent-harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -126,6 +126,7 @@ files:
|
|
|
126
126
|
- lib/agent_harness/providers/kilocode.rb
|
|
127
127
|
- lib/agent_harness/providers/mistral_vibe.rb
|
|
128
128
|
- lib/agent_harness/providers/opencode.rb
|
|
129
|
+
- lib/agent_harness/providers/rate_limit_reset_parsing.rb
|
|
129
130
|
- lib/agent_harness/providers/registry.rb
|
|
130
131
|
- lib/agent_harness/providers/token_usage_parsing.rb
|
|
131
132
|
- lib/agent_harness/response.rb
|