agent-harness 0.5.9 → 0.6.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 +7 -0
- data/lib/agent_harness/providers/aider.rb +361 -4
- data/lib/agent_harness/providers/codex.rb +38 -4
- data/lib/agent_harness/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11c53accd50a5842f5f67a3ef1adb97e2d25539966e08cf5ed1a2659b047ce0b
|
|
4
|
+
data.tar.gz: 562f0baad4bdc24dda2dcb7d65ef93d7657be1e64e2460400afc28e7ac419afa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9e7fb58eb6298e79f193b9de47c5fff995f92be7a9aff396277cf64489022d92030464b5cc08da506cc4496a4872dfc1fea1ca880f3d4cb82ad5133eaa65622
|
|
7
|
+
data.tar.gz: 40ca98102aedafdefb12d00794b721b0eb04f376eb7695034ca840ce17582abfb0741cd689f5c1a27b516acffa85399f9b58550fb516fe8e9e5652c3bb01d8d0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.9...agent-harness/v0.6.0) (2026-04-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **aider:** extract token usage via --llm-history-file ([0fff343](https://github.com/viamin/agent-harness/commit/0fff343f943d93899d0222b16ffa9832611289ff)), closes [#100](https://github.com/viamin/agent-harness/issues/100)
|
|
9
|
+
|
|
3
10
|
## [0.5.9](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.8...agent-harness/v0.5.9) (2026-04-12)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
3
7
|
module AgentHarness
|
|
4
8
|
module Providers
|
|
5
9
|
# Aider AI coding assistant provider
|
|
@@ -192,29 +196,382 @@ module AgentHarness
|
|
|
192
196
|
["--restore-chat-history", session_id]
|
|
193
197
|
end
|
|
194
198
|
|
|
199
|
+
def send_message(prompt:, **options)
|
|
200
|
+
log_debug("send_message_start", prompt_length: prompt.length, options: options.keys)
|
|
201
|
+
|
|
202
|
+
options = normalize_provider_runtime(options)
|
|
203
|
+
runtime = options[:provider_runtime]
|
|
204
|
+
|
|
205
|
+
options = normalize_mcp_servers(options)
|
|
206
|
+
validate_mcp_servers!(options[:mcp_servers]) if options[:mcp_servers]&.any?
|
|
207
|
+
|
|
208
|
+
llm_history_path = generate_llm_history_path
|
|
209
|
+
command = build_command(prompt, options.merge(llm_history_path: llm_history_path))
|
|
210
|
+
preparation = build_execution_preparation(options)
|
|
211
|
+
timeout = options[:timeout] || @config.timeout || default_timeout
|
|
212
|
+
|
|
213
|
+
start_time = Time.now
|
|
214
|
+
result = execute_with_timeout(
|
|
215
|
+
command,
|
|
216
|
+
timeout: timeout,
|
|
217
|
+
env: build_env(options),
|
|
218
|
+
preparation: preparation,
|
|
219
|
+
**command_execution_options(options)
|
|
220
|
+
)
|
|
221
|
+
duration = Time.now - start_time
|
|
222
|
+
|
|
223
|
+
response = parse_response(result, duration: duration, llm_history_path: llm_history_path)
|
|
224
|
+
if runtime&.model
|
|
225
|
+
response = Response.new(
|
|
226
|
+
output: response.output,
|
|
227
|
+
exit_code: response.exit_code,
|
|
228
|
+
duration: response.duration,
|
|
229
|
+
provider: response.provider,
|
|
230
|
+
model: runtime.model,
|
|
231
|
+
tokens: response.tokens,
|
|
232
|
+
metadata: response.metadata,
|
|
233
|
+
error: response.error
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
track_tokens(response) if response.tokens
|
|
238
|
+
|
|
239
|
+
log_debug("send_message_complete", duration: duration, tokens: response.tokens)
|
|
240
|
+
|
|
241
|
+
response
|
|
242
|
+
rescue McpConfigurationError, McpUnsupportedError, McpTransportUnsupportedError
|
|
243
|
+
raise
|
|
244
|
+
rescue => e
|
|
245
|
+
handle_error(e, prompt: prompt, options: options)
|
|
246
|
+
ensure
|
|
247
|
+
cleanup_llm_history_file!(llm_history_path)
|
|
248
|
+
end
|
|
249
|
+
|
|
195
250
|
protected
|
|
196
251
|
|
|
197
252
|
def build_command(prompt, options)
|
|
198
253
|
cmd = [self.class.binary_name]
|
|
254
|
+
runtime = options[:provider_runtime]
|
|
199
255
|
|
|
200
|
-
# Run in non-interactive mode
|
|
201
256
|
cmd << "--yes"
|
|
202
257
|
|
|
203
|
-
if
|
|
204
|
-
cmd += ["--
|
|
258
|
+
if options[:llm_history_path]
|
|
259
|
+
cmd += ["--llm-history-file", options[:llm_history_path]]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
model = runtime&.model || @config.model
|
|
263
|
+
if model && !model.empty?
|
|
264
|
+
cmd += ["--model", model]
|
|
205
265
|
end
|
|
206
266
|
|
|
207
267
|
if options[:session]
|
|
208
268
|
cmd += session_flags(options[:session])
|
|
209
269
|
end
|
|
210
270
|
|
|
271
|
+
if runtime&.flags&.any?
|
|
272
|
+
validate_runtime_flags!(runtime.flags)
|
|
273
|
+
cmd += runtime.flags
|
|
274
|
+
end
|
|
275
|
+
|
|
211
276
|
cmd += ["--message", prompt]
|
|
212
277
|
|
|
213
278
|
cmd
|
|
214
279
|
end
|
|
215
280
|
|
|
281
|
+
def parse_response(result, duration:, llm_history_path: nil)
|
|
282
|
+
response = super(result, duration: duration)
|
|
283
|
+
tokens = parse_token_usage(result, llm_history_path: llm_history_path)
|
|
284
|
+
|
|
285
|
+
return response unless tokens
|
|
286
|
+
|
|
287
|
+
Response.new(
|
|
288
|
+
output: response.output,
|
|
289
|
+
exit_code: response.exit_code,
|
|
290
|
+
duration: response.duration,
|
|
291
|
+
provider: response.provider,
|
|
292
|
+
model: response.model,
|
|
293
|
+
tokens: tokens,
|
|
294
|
+
metadata: response.metadata,
|
|
295
|
+
error: response.error
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
216
299
|
def default_timeout
|
|
217
|
-
600
|
|
300
|
+
600
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
TOKEN_COUNT_PATTERN = /\d[\d,]*(?:\.\d+)?[kmb]?/i
|
|
306
|
+
|
|
307
|
+
TOKEN_USAGE_PATTERN =
|
|
308
|
+
/^\s*Tokens:\s*(?<input>#{TOKEN_COUNT_PATTERN})\s+sent(?:,\s*#{TOKEN_COUNT_PATTERN}\s+cache\s+\w+)*,\s*(?<output>#{TOKEN_COUNT_PATTERN})\s+received\.?(?:\s+Cost:\s+.+)?\s*$/i
|
|
309
|
+
FOOTER_COST_PATTERN = /^\s*Cost:\s+.+\s*$/i
|
|
310
|
+
RUN_SHELL_COMMAND_PATTERN = /^\s*Run shell command\?.*$/i
|
|
311
|
+
OUTPUT_STATUS_PATTERN =
|
|
312
|
+
/^\s*(?:Applied edit to|Commit\b|Committing\b|You can use \/undo\b|Added .+ to the chat\.|Removed .+ from the chat\.|Use \/help\b|Create new file\?|Allow edits to\b|Edit the files\?|Run shell command\?).*$/i
|
|
313
|
+
OUTPUT_PATH_PATTERN = /\A(?:\.\.?\/|\/|~\/)[\w.\-\/]+\z/
|
|
314
|
+
OUTPUT_DOTFILE_PATTERN = /\A\.[\w.-]+\z/
|
|
315
|
+
OUTPUT_FILENAME_PATTERN = /\A[\w.-]+\.[A-Za-z][\w.-]*\z/
|
|
316
|
+
COMMON_SHELL_COMMAND_PATTERN =
|
|
317
|
+
/\A(?:git|bundle|ruby|python\d*(?:\.\d+)?|uv|npm|yarn|pnpm|node|bash|sh|zsh|make|rake|rspec|rails|go|pytest|bin\/[\w.-]+|sed|rg|grep|find|ls|cat|cp|mv|rm|mkdir|touch|chmod|chown|docker|kubectl)\z/
|
|
318
|
+
EXECUTOR_LLM_HISTORY_TIMEOUT = 10
|
|
319
|
+
|
|
320
|
+
def generate_llm_history_path
|
|
321
|
+
return "/tmp/aider_llm_history_#{Process.pid}_#{SecureRandom.hex(8)}" if sandboxed_environment?
|
|
322
|
+
|
|
323
|
+
File.join(Dir.tmpdir, "aider_llm_history_#{Process.pid}_#{SecureRandom.hex(8)}")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def parse_token_usage(result, llm_history_path:)
|
|
327
|
+
# Aider 0.86.x writes --llm-history-file as conversation text, not JSONL.
|
|
328
|
+
# Prefer the request-local history file when it includes a token report,
|
|
329
|
+
# but fall back to captured command output because the usage summary is
|
|
330
|
+
# printed there during normal runs.
|
|
331
|
+
parse_token_usage_text(safe_read_llm_history(llm_history_path), source: :history) ||
|
|
332
|
+
parse_token_usage_text(result.stdout, source: :output) ||
|
|
333
|
+
parse_token_usage_text(result.stderr, source: :output)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def read_llm_history(path)
|
|
337
|
+
return read_executor_llm_history(path) if sandboxed_environment?
|
|
338
|
+
return nil unless path && File.exist?(path) && !File.zero?(path)
|
|
339
|
+
|
|
340
|
+
content = File.read(path)
|
|
341
|
+
return nil if content.strip.empty?
|
|
342
|
+
|
|
343
|
+
content
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def safe_read_llm_history(path)
|
|
347
|
+
read_llm_history(path)
|
|
348
|
+
rescue => e
|
|
349
|
+
log_debug("llm_history_parse_error", error: e.message)
|
|
350
|
+
nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def parse_token_usage_text(content, source: :output)
|
|
354
|
+
return nil if content.nil? || content.strip.empty?
|
|
355
|
+
|
|
356
|
+
match = if source == :history
|
|
357
|
+
extract_history_token_usage_match(content)
|
|
358
|
+
else
|
|
359
|
+
extract_output_token_usage_match(content)
|
|
360
|
+
end
|
|
361
|
+
return nil unless match
|
|
362
|
+
|
|
363
|
+
input = parse_token_count(match[:input])
|
|
364
|
+
output = parse_token_count(match[:output])
|
|
365
|
+
|
|
366
|
+
{input: input, output: output, total: input + output}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def extract_history_token_usage_match(content)
|
|
370
|
+
lines = content.lines
|
|
371
|
+
|
|
372
|
+
lines.each_index.reverse_each do |index|
|
|
373
|
+
match = TOKEN_USAGE_PATTERN.match(lines[index])
|
|
374
|
+
next unless match
|
|
375
|
+
next unless history_token_usage_footer_line?(lines, index)
|
|
376
|
+
|
|
377
|
+
return match
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def extract_output_token_usage_match(content)
|
|
384
|
+
lines = content.lines
|
|
385
|
+
|
|
386
|
+
lines.each_index.reverse_each do |index|
|
|
387
|
+
match = TOKEN_USAGE_PATTERN.match(lines[index])
|
|
388
|
+
next unless match
|
|
389
|
+
next unless output_token_usage_footer_line?(lines, index)
|
|
390
|
+
|
|
391
|
+
return match
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def history_token_usage_footer_line?(lines, index)
|
|
398
|
+
footer_prefix?(lines, index) && footer_suffix?(lines, index)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def output_token_usage_footer_line?(lines, index)
|
|
402
|
+
footer_prefix?(lines, index) && output_footer_suffix?(lines, index)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def footer_prefix?(lines, index)
|
|
406
|
+
block_start = index
|
|
407
|
+
while block_start.positive? && TOKEN_USAGE_PATTERN.match?(lines[block_start - 1])
|
|
408
|
+
block_start -= 1
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
return false if block_start.zero?
|
|
412
|
+
|
|
413
|
+
lines[block_start - 1].strip.empty?
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def footer_suffix?(lines, index)
|
|
417
|
+
lines[(index + 1)..].to_a.all? do |line|
|
|
418
|
+
stripped = line.strip
|
|
419
|
+
stripped.empty? || TOKEN_USAGE_PATTERN.match?(line) || FOOTER_COST_PATTERN.match?(line)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def output_footer_suffix?(lines, index)
|
|
424
|
+
suffix_lines = lines[(index + 1)..].to_a
|
|
425
|
+
shell_prompt_index = suffix_lines.index { |line| RUN_SHELL_COMMAND_PATTERN.match?(line) }
|
|
426
|
+
|
|
427
|
+
suffix_lines.each_with_index.all? do |line, line_index|
|
|
428
|
+
stripped = line.strip
|
|
429
|
+
stripped.empty? ||
|
|
430
|
+
TOKEN_USAGE_PATTERN.match?(line) ||
|
|
431
|
+
FOOTER_COST_PATTERN.match?(line) ||
|
|
432
|
+
OUTPUT_STATUS_PATTERN.match?(line) ||
|
|
433
|
+
output_path_footer_line?(stripped) ||
|
|
434
|
+
output_command_footer_line?(line, line_index, shell_prompt_index)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def output_path_footer_line?(line)
|
|
439
|
+
OUTPUT_PATH_PATTERN.match?(line) ||
|
|
440
|
+
OUTPUT_DOTFILE_PATTERN.match?(line) ||
|
|
441
|
+
OUTPUT_FILENAME_PATTERN.match?(line) ||
|
|
442
|
+
(line.include?("/") && line.match?(/\A[\w.\-\/]+\z/))
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def output_command_footer_line?(line, line_index, shell_prompt_index)
|
|
446
|
+
return false unless shell_prompt_index && line_index < shell_prompt_index
|
|
447
|
+
|
|
448
|
+
stripped = line.strip
|
|
449
|
+
return false if stripped.end_with?(".", "?", "!")
|
|
450
|
+
return false if stripped.empty?
|
|
451
|
+
|
|
452
|
+
tokens = shell_command_footer_tokens(stripped)
|
|
453
|
+
return false if tokens.empty?
|
|
454
|
+
command = tokens.first
|
|
455
|
+
return false unless command_invocation_token?(command)
|
|
456
|
+
return single_token_command_footer?(command) if tokens.length == 1
|
|
457
|
+
return false unless command_line_token?(command, tokens[1..])
|
|
458
|
+
|
|
459
|
+
tokens[1..].all? { |token| command_argument_token?(token) }
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def shell_command_footer_tokens(line)
|
|
463
|
+
Shellwords.shellsplit(line.sub(/\A[$>#]\s*/, ""))
|
|
464
|
+
rescue ArgumentError
|
|
465
|
+
[]
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def command_token?(token)
|
|
469
|
+
token.match?(/\A[a-z0-9_][\w.\/~:-]*\z/) && token.match?(/[a-z]/)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def command_invocation_token?(token)
|
|
473
|
+
command_token?(token) || executable_path_token?(token)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def executable_path_token?(token)
|
|
477
|
+
token.match?(%r{\A(?:\.\.?/|/|~/)[\w.+%:@=-][\w./+%:@~=-]*\z})
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def command_line_token?(token, arguments)
|
|
481
|
+
command_invocation_token?(token) &&
|
|
482
|
+
(COMMON_SHELL_COMMAND_PATTERN.match?(token) ||
|
|
483
|
+
executable_path_token?(token) ||
|
|
484
|
+
command_footer_shell_like_arguments?(arguments))
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def single_token_command_footer?(token)
|
|
488
|
+
COMMON_SHELL_COMMAND_PATTERN.match?(token) || executable_path_token?(token)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def command_footer_shell_like_arguments?(arguments)
|
|
492
|
+
arguments.any? do |argument|
|
|
493
|
+
argument.match?(%r{\A(?:&&|\|\|?|\||[<>]|>>|&>|2>)\z}) ||
|
|
494
|
+
argument.start_with?("-", "./", "../", "/", "~/") ||
|
|
495
|
+
argument.include?("/")
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def command_argument_token?(token)
|
|
500
|
+
!token.empty? && !token.match?(/[[:cntrl:]]/)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def parse_token_count(value)
|
|
504
|
+
normalized = value.delete(",").downcase
|
|
505
|
+
multiplier = case normalized[-1]
|
|
506
|
+
when "k" then 1_000
|
|
507
|
+
when "m" then 1_000_000
|
|
508
|
+
when "b" then 1_000_000_000
|
|
509
|
+
else 1
|
|
510
|
+
end
|
|
511
|
+
normalized = normalized[0...-1] if multiplier > 1
|
|
512
|
+
|
|
513
|
+
(normalized.to_f * multiplier).round
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def cleanup_llm_history_file!(path)
|
|
517
|
+
return unless path
|
|
518
|
+
|
|
519
|
+
return cleanup_executor_llm_history_file!(path) if sandboxed_environment?
|
|
520
|
+
|
|
521
|
+
File.delete(path) if File.exist?(path)
|
|
522
|
+
rescue => e
|
|
523
|
+
log_debug("llm_history_cleanup_error", error: e.message)
|
|
524
|
+
nil
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def validate_runtime_flags!(flags)
|
|
528
|
+
invalid_flags = reserved_runtime_flags(flags)
|
|
529
|
+
return if invalid_flags.empty?
|
|
530
|
+
|
|
531
|
+
raise ArgumentError,
|
|
532
|
+
"Aider provider_runtime.flags cannot override provider-managed flags: " \
|
|
533
|
+
"#{invalid_flags.join(", ")}"
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def reserved_runtime_flags(flags)
|
|
537
|
+
flags.each_with_index.filter_map do |flag, index|
|
|
538
|
+
next unless reserved_runtime_flag?(flag)
|
|
539
|
+
|
|
540
|
+
if flag == "--llm-history-file" && flags[index + 1]
|
|
541
|
+
"#{flag} #{flags[index + 1]}"
|
|
542
|
+
else
|
|
543
|
+
flag
|
|
544
|
+
end
|
|
545
|
+
end.uniq
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def reserved_runtime_flag?(flag)
|
|
549
|
+
flag == "--llm-history-file" || flag.start_with?("--llm-history-file=")
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def read_executor_llm_history(path)
|
|
553
|
+
return nil unless path
|
|
554
|
+
|
|
555
|
+
result = @executor.execute(
|
|
556
|
+
["sh", "-lc", "if [ -s #{Shellwords.escape(path)} ]; then cat #{Shellwords.escape(path)}; fi"],
|
|
557
|
+
timeout: EXECUTOR_LLM_HISTORY_TIMEOUT
|
|
558
|
+
)
|
|
559
|
+
return nil unless result.success?
|
|
560
|
+
|
|
561
|
+
content = result.stdout
|
|
562
|
+
return nil if content.to_s.strip.empty?
|
|
563
|
+
|
|
564
|
+
content
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def cleanup_executor_llm_history_file!(path)
|
|
568
|
+
@executor.execute(
|
|
569
|
+
["sh", "-lc", "rm -f -- #{Shellwords.escape(path)}"],
|
|
570
|
+
timeout: EXECUTOR_LLM_HISTORY_TIMEOUT
|
|
571
|
+
)
|
|
572
|
+
rescue => e
|
|
573
|
+
log_debug("llm_history_cleanup_error", error: e.message)
|
|
574
|
+
nil
|
|
218
575
|
end
|
|
219
576
|
end
|
|
220
577
|
end
|
|
@@ -10,6 +10,29 @@ module AgentHarness
|
|
|
10
10
|
class Codex < Base
|
|
11
11
|
SUPPORTED_CLI_VERSION = "0.116.0"
|
|
12
12
|
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.117.0").freeze
|
|
13
|
+
OAUTH_REFRESH_FAILURE_PATTERNS = [
|
|
14
|
+
/refresh_token_reused/i,
|
|
15
|
+
/failed to refresh token\b.*\b401\b/im,
|
|
16
|
+
/failed to refresh token\b.*unauthorized/im,
|
|
17
|
+
/failed to refresh token\b.*\binvalid_client\b/im,
|
|
18
|
+
/failed to refresh token\b.*\binvalid_grant\b/im,
|
|
19
|
+
/failed to refresh token\b.*invalid.*refresh.*token/im,
|
|
20
|
+
/failed to refresh token\b.*refresh.*token.*invalid/im,
|
|
21
|
+
/your access token could not be refreshed because\b.*\b401\b/im,
|
|
22
|
+
/your access token could not be refreshed because\b.*unauthorized/im,
|
|
23
|
+
/your access token could not be refreshed because\b.*\binvalid_client\b/im,
|
|
24
|
+
/your access token could not be refreshed because\b.*\binvalid_grant\b/im,
|
|
25
|
+
/your access token could not be refreshed because\b.*invalid.*refresh.*token/im,
|
|
26
|
+
/your access token could not be refreshed because\b.*refresh.*token.*invalid/im,
|
|
27
|
+
/your access token could not be refreshed because\s+your refresh token .*already (?:been )?used/im,
|
|
28
|
+
/refresh token .*already (?:been )?used/im
|
|
29
|
+
].freeze
|
|
30
|
+
OAUTH_REFRESH_TRANSIENT_PATTERNS = [
|
|
31
|
+
/your access token could not be refreshed because\s+(?:the\s+)?auth(?:entication)? service(?:\s+(?:is|was))?\s+(?:temporarily\s+)?unavailable/im,
|
|
32
|
+
/your access token could not be refreshed because .*connection.*error/im,
|
|
33
|
+
/failed to refresh token\b.*connection.*error/im,
|
|
34
|
+
/failed to refresh token\b.*service(?:\s+(?:is|was))?\s+(?:temporarily\s+)?unavailable/im
|
|
35
|
+
].freeze
|
|
13
36
|
|
|
14
37
|
class << self
|
|
15
38
|
def provider_name
|
|
@@ -171,15 +194,26 @@ module AgentHarness
|
|
|
171
194
|
end
|
|
172
195
|
|
|
173
196
|
def error_patterns
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
{
|
|
198
|
+
rate_limited: COMMON_ERROR_PATTERNS[:rate_limited],
|
|
199
|
+
timeout: [
|
|
200
|
+
/your access token could not be refreshed.*(?:timeout|timed.?out)/im,
|
|
201
|
+
/failed to refresh token\b.*(?:timeout|timed.?out)/im
|
|
202
|
+
],
|
|
203
|
+
transient: COMMON_ERROR_PATTERNS[:transient] + [
|
|
204
|
+
/connection.*reset/i
|
|
205
|
+
] + OAUTH_REFRESH_TRANSIENT_PATTERNS,
|
|
206
|
+
auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [
|
|
207
|
+
/\b401\b/,
|
|
208
|
+
/incorrect.*api.*key/i
|
|
209
|
+
] + OAUTH_REFRESH_FAILURE_PATTERNS,
|
|
210
|
+
quota_exceeded: COMMON_ERROR_PATTERNS[:quota_exceeded],
|
|
177
211
|
sandbox_failure: [
|
|
178
212
|
/bwrap.*no permissions/i,
|
|
179
213
|
/no permissions to create a new namespace/i,
|
|
180
214
|
/unprivileged.*namespace/i
|
|
181
215
|
]
|
|
182
|
-
|
|
216
|
+
}
|
|
183
217
|
end
|
|
184
218
|
|
|
185
219
|
def auth_status
|