agent-harness 0.9.0 → 0.11.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 +23 -0
- data/README.md +36 -5
- data/lib/agent_harness/authentication.rb +47 -7
- data/lib/agent_harness/conversation.rb +326 -0
- data/lib/agent_harness/errors.rb +3 -0
- data/lib/agent_harness/mcp_server.rb +32 -0
- data/lib/agent_harness/openai_compatible_transport.rb +391 -0
- data/lib/agent_harness/provider_runtime.rb +40 -4
- data/lib/agent_harness/providers/adapter.rb +62 -3
- data/lib/agent_harness/providers/anthropic.rb +30 -0
- data/lib/agent_harness/providers/base.rb +142 -0
- data/lib/agent_harness/providers/codex.rb +26 -3
- data/lib/agent_harness/providers/github_copilot.rb +130 -74
- data/lib/agent_harness/text_transport.rb +320 -13
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +28 -2
- metadata +3 -1
|
@@ -181,6 +181,65 @@ module AgentHarness
|
|
|
181
181
|
handle_error(e, prompt: prompt, options: options)
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
+
# Send a multi-turn chat message via the provider's chat transport.
|
|
185
|
+
#
|
|
186
|
+
# Providers that support chat mode can accept either +conversation:+
|
|
187
|
+
# or +messages:+ as the conversation history payload.
|
|
188
|
+
#
|
|
189
|
+
# Structured streaming events are delivered through three channels:
|
|
190
|
+
# - +on_chat_chunk+ proc (keyword argument)
|
|
191
|
+
# - +observer+ object responding to +on_chat_chunk+
|
|
192
|
+
# - block (yield)
|
|
193
|
+
#
|
|
194
|
+
# When multiple receivers are provided, all receive every event.
|
|
195
|
+
#
|
|
196
|
+
# @param conversation [Array<Hash>, nil] message history
|
|
197
|
+
# @param messages [Array<Hash>, nil] alias for +conversation+
|
|
198
|
+
# @param tools [Array<Hash>, nil] tool/function definitions
|
|
199
|
+
# @param stream [Boolean] whether to stream the response
|
|
200
|
+
# @param on_chat_chunk [Proc, nil] callback for structured streaming events
|
|
201
|
+
# @param observer [#on_chat_chunk, nil] observer receiving streaming events
|
|
202
|
+
# @param options [Hash] additional options
|
|
203
|
+
# @yield [Hash] streaming chunks when stream: true
|
|
204
|
+
# @return [Response] the response
|
|
205
|
+
# @raise [ProviderError] if the provider does not support chat mode
|
|
206
|
+
def send_chat_message(conversation: nil, messages: nil, tools: nil, stream: false,
|
|
207
|
+
on_chat_chunk: nil, observer: nil, **options, &on_chunk)
|
|
208
|
+
unless supports_chat?
|
|
209
|
+
raise ProviderError, "#{name} does not support chat mode"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
options = normalize_provider_runtime(options)
|
|
213
|
+
runtime = options[:provider_runtime]
|
|
214
|
+
conversation ||= messages
|
|
215
|
+
raise ArgumentError, "conversation or messages is required" unless conversation
|
|
216
|
+
tools = runtime.chat_tools if tools.nil? && runtime&.chat_tools
|
|
217
|
+
|
|
218
|
+
transport = resolve_chat_transport(options)
|
|
219
|
+
messages = format_messages_for_transport(conversation, transport)
|
|
220
|
+
transport_opts = chat_transport_options(runtime, options)
|
|
221
|
+
transport_opts[:on_chat_chunk] = on_chat_chunk if on_chat_chunk
|
|
222
|
+
transport_opts[:observer] = observer if observer
|
|
223
|
+
|
|
224
|
+
response = transport.chat(
|
|
225
|
+
messages: messages,
|
|
226
|
+
tools: tools,
|
|
227
|
+
stream: stream,
|
|
228
|
+
**transport_opts,
|
|
229
|
+
&on_chunk
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
track_tokens(response) if response.tokens
|
|
233
|
+
log_debug("send_chat_message_complete", duration: response.duration, tokens: response.tokens)
|
|
234
|
+
|
|
235
|
+
response
|
|
236
|
+
rescue ProviderError, AuthenticationError, RateLimitError, TimeoutError
|
|
237
|
+
raise
|
|
238
|
+
rescue => e
|
|
239
|
+
last_msg = conversation&.last || messages&.last
|
|
240
|
+
handle_error(e, prompt: (last_msg&.dig(:content) || last_msg&.dig("content")).to_s, options: options)
|
|
241
|
+
end
|
|
242
|
+
|
|
184
243
|
# Provider name for display
|
|
185
244
|
#
|
|
186
245
|
# @return [String] display name
|
|
@@ -466,6 +525,89 @@ module AgentHarness
|
|
|
466
525
|
end
|
|
467
526
|
end
|
|
468
527
|
|
|
528
|
+
def resolve_chat_transport(options)
|
|
529
|
+
runtime = options[:provider_runtime]
|
|
530
|
+
|
|
531
|
+
# When the runtime specifies chat-specific overrides (base_url, api_key),
|
|
532
|
+
# build a fresh transport instead of reusing the memoized default.
|
|
533
|
+
if runtime && (runtime.chat_base_url || runtime.chat_api_key)
|
|
534
|
+
transport = build_runtime_chat_transport(runtime)
|
|
535
|
+
if transport
|
|
536
|
+
return transport
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
transport = chat_transport
|
|
541
|
+
raise ProviderError, "#{name} chat_transport returned nil" unless transport
|
|
542
|
+
|
|
543
|
+
transport
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Build a one-off chat transport from ProviderRuntime overrides.
|
|
547
|
+
#
|
|
548
|
+
# Subclasses that support chat must override this when the runtime
|
|
549
|
+
# carries chat_base_url or chat_api_key so those overrides are
|
|
550
|
+
# actually applied. The base implementation raises to surface the
|
|
551
|
+
# misconfiguration early rather than silently ignoring the overrides.
|
|
552
|
+
def build_runtime_chat_transport(_runtime)
|
|
553
|
+
raise ProviderError,
|
|
554
|
+
"#{name} does not support chat_base_url/chat_api_key overrides on ProviderRuntime"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def format_messages_for_transport(conversation, transport)
|
|
558
|
+
normalized = conversation.map { |msg| normalize_transport_message(msg) }
|
|
559
|
+
return normalized unless anthropic_transport?(transport)
|
|
560
|
+
return normalized unless anthropic_conversion_required?(normalized)
|
|
561
|
+
|
|
562
|
+
anthropic = anthropic_conversation(normalized)
|
|
563
|
+
system_messages = anthropic[:system] ? [{role: "system", content: anthropic[:system]}] : []
|
|
564
|
+
|
|
565
|
+
system_messages + anthropic[:messages]
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def normalize_transport_message(message)
|
|
569
|
+
message.each_with_object({}) do |(key, value), memo|
|
|
570
|
+
memo[key.is_a?(String) ? key.to_sym : key] = value
|
|
571
|
+
end.tap do |normalized|
|
|
572
|
+
normalized[:role] = normalized[:role].to_s if normalized.key?(:role)
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def anthropic_transport?(transport)
|
|
577
|
+
chat_transport_type == :anthropic || transport.is_a?(TextTransport)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def anthropic_conversion_required?(messages)
|
|
581
|
+
messages.any? do |msg|
|
|
582
|
+
msg[:role] == "tool" || msg.key?(:tool_calls)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def anthropic_conversation(messages)
|
|
587
|
+
conversation = Conversation.new
|
|
588
|
+
|
|
589
|
+
messages.each do |msg|
|
|
590
|
+
conversation.add_message(
|
|
591
|
+
msg.fetch(:role).to_sym,
|
|
592
|
+
msg[:content],
|
|
593
|
+
tool_calls: msg[:tool_calls],
|
|
594
|
+
tool_call_id: msg[:tool_call_id]
|
|
595
|
+
)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
conversation.to_anthropic_messages
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def chat_transport_options(runtime, options)
|
|
602
|
+
opts = {}
|
|
603
|
+
max_tok = options[:chat_max_tokens] || options[:max_tokens] || runtime&.chat_max_tokens
|
|
604
|
+
opts[:max_tokens] = max_tok if max_tok
|
|
605
|
+
model = runtime&.chat_model || runtime&.model
|
|
606
|
+
opts[:model] = model if model
|
|
607
|
+
opts[:temperature] = options[:temperature] if options[:temperature]
|
|
608
|
+
opts
|
|
609
|
+
end
|
|
610
|
+
|
|
469
611
|
def log_debug(action, **context)
|
|
470
612
|
@logger&.debug("[AgentHarness::#{self.class.provider_name}] #{action}: #{context.inspect}")
|
|
471
613
|
end
|
|
@@ -139,6 +139,28 @@ module AgentHarness
|
|
|
139
139
|
def smoke_test_contract
|
|
140
140
|
Base::DEFAULT_SMOKE_TEST_CONTRACT
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
def parse_cli_jsonl_transcript(raw_output, max_events: nil)
|
|
144
|
+
return new.send(:parse_jsonl_output, "") if max_events && max_events <= 0
|
|
145
|
+
|
|
146
|
+
output = max_events ? tail_nonempty_lines(raw_output, limit: max_events).join("\n") : raw_output
|
|
147
|
+
|
|
148
|
+
new.send(:parse_jsonl_output, output)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def tail_nonempty_lines(text, limit:)
|
|
154
|
+
return [] if limit <= 0
|
|
155
|
+
|
|
156
|
+
text.to_s.each_line.each_with_object([]) do |line, lines|
|
|
157
|
+
stripped = line.strip
|
|
158
|
+
next if stripped.empty?
|
|
159
|
+
|
|
160
|
+
lines.shift if lines.size >= limit
|
|
161
|
+
lines << stripped
|
|
162
|
+
end
|
|
163
|
+
end
|
|
142
164
|
end
|
|
143
165
|
|
|
144
166
|
def name
|
|
@@ -603,10 +625,11 @@ module AgentHarness
|
|
|
603
625
|
when "turn.completed"
|
|
604
626
|
turn_usage = build_token_usage(event["usage"])
|
|
605
627
|
result = event["result"]
|
|
628
|
+
result_parts = result.is_a?(String) ? [result] : extract_task_complete_parts(event)
|
|
606
629
|
wrapped_completion_without_new_output =
|
|
607
630
|
pending_turn_usage_source == :wrapped &&
|
|
608
631
|
pending_turn_usage &&
|
|
609
|
-
|
|
632
|
+
result_parts.nil? &&
|
|
610
633
|
(turn_usage.nil? || current_turn_parts.empty? || current_turn_parts.equal?(pending_wrapped_output_parts))
|
|
611
634
|
|
|
612
635
|
if wrapped_completion_without_new_output
|
|
@@ -663,8 +686,8 @@ module AgentHarness
|
|
|
663
686
|
pending_wrapped_same_turn_finalization = false
|
|
664
687
|
end
|
|
665
688
|
|
|
666
|
-
if
|
|
667
|
-
current_turn_parts =
|
|
689
|
+
if result_parts
|
|
690
|
+
current_turn_parts = result_parts
|
|
668
691
|
saw_assistant_output = true
|
|
669
692
|
current_turn_finalized_output = true
|
|
670
693
|
end
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "json"
|
|
5
|
+
require "pathname"
|
|
5
6
|
|
|
6
7
|
module AgentHarness
|
|
7
8
|
module Providers
|
|
8
9
|
class GithubCopilot < Base
|
|
9
10
|
include TokenUsageParsing
|
|
10
11
|
|
|
11
|
-
PACKAGE_NAME = "@githubnext/github-copilot-cli"
|
|
12
|
-
SUPPORTED_CLI_VERSION = "0.1.36"
|
|
13
|
-
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.2.0").freeze
|
|
14
|
-
|
|
15
12
|
MODEL_PATTERN = /^gpt-[\d.o-]+(?:-turbo)?(?:-mini)?$/i
|
|
16
13
|
JSON_OUTPUT_MIN_VERSION = Gem::Version.new("0.0.422").freeze
|
|
14
|
+
SUBCOMMAND_CLI_MIN_VERSION = Gem::Version.new("0.1.0").freeze
|
|
15
|
+
UNSUPPORTED_SUBCOMMAND_CLI_MESSAGE =
|
|
16
|
+
"github-copilot-cli 0.1.x does not expose a non-interactive send interface; " \
|
|
17
|
+
"the what-the-shell subcommand is interactive and cannot be used by AgentHarness."
|
|
17
18
|
|
|
18
19
|
SMOKE_TEST_CONTRACT = {
|
|
19
20
|
prompt: "Reply with exactly OK.",
|
|
@@ -34,41 +35,22 @@ module AgentHarness
|
|
|
34
35
|
|
|
35
36
|
def available?
|
|
36
37
|
executor = AgentHarness.configuration.command_executor
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
41
|
-
version = version.strip if version.respond_to?(:strip)
|
|
42
|
-
validate_install_version!(version)
|
|
43
|
-
package_spec = "#{PACKAGE_NAME}@#{version}".freeze
|
|
44
|
-
install_command_prefix = ["npm", "install", "-g", "--ignore-scripts"].freeze
|
|
45
|
-
install_command = (install_command_prefix + [package_spec]).freeze
|
|
46
|
-
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
47
|
-
.map { |op, ver| "#{op} #{ver}".freeze }
|
|
48
|
-
.freeze
|
|
49
|
-
|
|
50
|
-
contract = {
|
|
51
|
-
source: {
|
|
52
|
-
type: :npm,
|
|
53
|
-
package: PACKAGE_NAME
|
|
54
|
-
}.freeze,
|
|
55
|
-
install_command_prefix: install_command_prefix,
|
|
56
|
-
install_command: install_command,
|
|
57
|
-
binary_name: binary_name,
|
|
58
|
-
default_version: SUPPORTED_CLI_VERSION,
|
|
59
|
-
version: version,
|
|
60
|
-
version_requirement: version_requirement,
|
|
61
|
-
supported_version_requirement: SUPPORTED_CLI_REQUIREMENT.to_s
|
|
62
|
-
}
|
|
38
|
+
return false unless executor.which(binary_name)
|
|
63
39
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
contract.freeze
|
|
40
|
+
!subcommand_cli_version?(copilot_cli_version(executor: executor))
|
|
41
|
+
rescue
|
|
42
|
+
false
|
|
68
43
|
end
|
|
69
44
|
|
|
70
|
-
def
|
|
71
|
-
|
|
45
|
+
def installation_contract(version: nil)
|
|
46
|
+
# The published @githubnext/github-copilot-cli package only has
|
|
47
|
+
# 0.1.x releases, and those expose an interactive subcommand instead
|
|
48
|
+
# of the non-interactive -p prompt path AgentHarness uses.
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def install_command(version: nil)
|
|
53
|
+
installation_contract(version: version)&.fetch(:install_command)
|
|
72
54
|
end
|
|
73
55
|
|
|
74
56
|
def provider_metadata_overrides
|
|
@@ -116,6 +98,10 @@ module AgentHarness
|
|
|
116
98
|
]
|
|
117
99
|
end
|
|
118
100
|
|
|
101
|
+
def supports_chat?
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
119
105
|
def smoke_test_contract
|
|
120
106
|
SMOKE_TEST_CONTRACT
|
|
121
107
|
end
|
|
@@ -134,26 +120,26 @@ module AgentHarness
|
|
|
134
120
|
|
|
135
121
|
private
|
|
136
122
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
def copilot_cli_version(executor:)
|
|
124
|
+
result = executor.execute([binary_name, "--version"], timeout: 5, env: {})
|
|
125
|
+
extract_version(result)
|
|
126
|
+
rescue
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
143
129
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
end
|
|
130
|
+
def subcommand_cli_version?(version)
|
|
131
|
+
!version.nil? && version >= SUBCOMMAND_CLI_MIN_VERSION
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def extract_version(result)
|
|
135
|
+
return nil unless result.success?
|
|
151
136
|
|
|
152
|
-
|
|
137
|
+
version_string = [result.stdout, result.stderr].compact.join("\n")[/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?/]
|
|
138
|
+
return nil if version_string.nil? || version_string.empty?
|
|
153
139
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
140
|
+
Gem::Version.new(version_string)
|
|
141
|
+
rescue ArgumentError
|
|
142
|
+
nil
|
|
157
143
|
end
|
|
158
144
|
end
|
|
159
145
|
|
|
@@ -194,21 +180,59 @@ module AgentHarness
|
|
|
194
180
|
}
|
|
195
181
|
end
|
|
196
182
|
|
|
197
|
-
def dangerous_mode_flags(probe_timeout: nil, env: {})
|
|
198
|
-
|
|
183
|
+
def dangerous_mode_flags(probe_timeout: nil, env: {}, version: nil)
|
|
184
|
+
version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
|
|
185
|
+
return [] if subcommand_cli_version?(version)
|
|
186
|
+
return [] unless supports_json_output_format?(version: version)
|
|
199
187
|
|
|
200
188
|
["--allow-all"]
|
|
201
189
|
end
|
|
202
190
|
|
|
203
|
-
def supports_sessions?
|
|
204
|
-
|
|
191
|
+
def supports_sessions?(probe_timeout: nil, env: {}, version: nil)
|
|
192
|
+
legacy_prompt_cli?(version: version, probe_timeout: probe_timeout, env: env)
|
|
205
193
|
end
|
|
206
194
|
|
|
207
|
-
def session_flags(session_id)
|
|
195
|
+
def session_flags(session_id, version: nil, probe_timeout: nil, env: {})
|
|
208
196
|
return [] unless session_id && !session_id.empty?
|
|
197
|
+
return [] unless legacy_prompt_cli?(version: version, probe_timeout: probe_timeout, env: env)
|
|
198
|
+
|
|
209
199
|
["--resume", session_id]
|
|
210
200
|
end
|
|
211
201
|
|
|
202
|
+
GITHUB_MODELS_BASE_URL = "https://models.inference.ai.azure.com"
|
|
203
|
+
CHAT_DEFAULT_MODEL = "gpt-4o"
|
|
204
|
+
CHAT_MODELS = %w[gpt-4o gpt-4o-mini gpt-4-turbo].freeze
|
|
205
|
+
|
|
206
|
+
def supports_chat?
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def chat_models
|
|
211
|
+
CHAT_MODELS
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def chat_transport
|
|
215
|
+
@chat_transport ||= OpenAICompatibleTransport.new(
|
|
216
|
+
base_url: GITHUB_MODELS_BASE_URL,
|
|
217
|
+
api_key: resolve_chat_api_key,
|
|
218
|
+
model: CHAT_DEFAULT_MODEL,
|
|
219
|
+
logger: @logger
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_runtime_chat_transport(runtime)
|
|
224
|
+
OpenAICompatibleTransport.new(
|
|
225
|
+
base_url: runtime.chat_base_url || GITHUB_MODELS_BASE_URL,
|
|
226
|
+
api_key: runtime.chat_api_key || resolve_chat_api_key,
|
|
227
|
+
model: runtime.chat_model || runtime.model || CHAT_DEFAULT_MODEL,
|
|
228
|
+
logger: @logger
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def chat_transport_type
|
|
233
|
+
:openai_compatible
|
|
234
|
+
end
|
|
235
|
+
|
|
212
236
|
def auth_type
|
|
213
237
|
:oauth
|
|
214
238
|
end
|
|
@@ -221,7 +245,7 @@ module AgentHarness
|
|
|
221
245
|
output_format: :text,
|
|
222
246
|
sandbox_aware: false,
|
|
223
247
|
uses_subcommand: false,
|
|
224
|
-
non_interactive_flag:
|
|
248
|
+
non_interactive_flag: nil,
|
|
225
249
|
legitimate_exit_codes: [0],
|
|
226
250
|
stderr_is_diagnostic: true,
|
|
227
251
|
parses_rate_limit_reset: false
|
|
@@ -324,11 +348,15 @@ module AgentHarness
|
|
|
324
348
|
protected
|
|
325
349
|
|
|
326
350
|
def build_command(prompt, options)
|
|
327
|
-
cmd = [self.class.binary_name, "-p", prompt]
|
|
328
351
|
env = options.fetch(:_command_env) { build_env(options) }
|
|
329
352
|
runtime = options[:provider_runtime]
|
|
353
|
+
version = copilot_cli_version(probe_timeout: options[:_version_probe_timeout], env: env)
|
|
354
|
+
|
|
355
|
+
raise unsupported_subcommand_cli_error if subcommand_cli_version?(version)
|
|
330
356
|
|
|
331
|
-
|
|
357
|
+
cmd = [self.class.binary_name, "-p", prompt]
|
|
358
|
+
|
|
359
|
+
if supports_json_output_format?(version: version)
|
|
332
360
|
cmd += ["--output-format", "json"]
|
|
333
361
|
else
|
|
334
362
|
# Silent mode suppresses the model/stats decoration older CLIs print in
|
|
@@ -340,11 +368,11 @@ module AgentHarness
|
|
|
340
368
|
cmd += ["--model", model] if model
|
|
341
369
|
if options[:dangerous_mode] && supports_dangerous_mode?
|
|
342
370
|
cmd += programmatic_tool_approval_flags
|
|
343
|
-
cmd += dangerous_mode_flags(
|
|
371
|
+
cmd += dangerous_mode_flags(version: version)
|
|
344
372
|
end
|
|
345
373
|
|
|
346
374
|
if options[:session] && !options[:session].empty?
|
|
347
|
-
cmd += session_flags(options[:session])
|
|
375
|
+
cmd += session_flags(options[:session], version: version)
|
|
348
376
|
end
|
|
349
377
|
|
|
350
378
|
cmd
|
|
@@ -385,9 +413,22 @@ module AgentHarness
|
|
|
385
413
|
["--allow-all-tools"]
|
|
386
414
|
end
|
|
387
415
|
|
|
388
|
-
def supports_json_output_format?(probe_timeout: nil, env: {})
|
|
389
|
-
version
|
|
390
|
-
!version.nil? && version >= JSON_OUTPUT_MIN_VERSION
|
|
416
|
+
def supports_json_output_format?(probe_timeout: nil, env: {}, version: nil)
|
|
417
|
+
version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
|
|
418
|
+
!version.nil? && !subcommand_cli_version?(version) && version >= JSON_OUTPUT_MIN_VERSION
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def legacy_prompt_cli?(probe_timeout: nil, env: {}, version: nil)
|
|
422
|
+
version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
|
|
423
|
+
!version.nil? && !subcommand_cli_version?(version)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def subcommand_cli_version?(version)
|
|
427
|
+
self.class.send(:subcommand_cli_version?, version)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def unsupported_subcommand_cli_error
|
|
431
|
+
ProviderError.new(UNSUPPORTED_SUBCOMMAND_CLI_MESSAGE)
|
|
391
432
|
end
|
|
392
433
|
|
|
393
434
|
def copilot_cli_version(probe_timeout: nil, env: {})
|
|
@@ -443,14 +484,7 @@ module AgentHarness
|
|
|
443
484
|
end
|
|
444
485
|
|
|
445
486
|
def extract_version(result)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
version_string = [result.stdout, result.stderr].compact.join("\n")[/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?/]
|
|
449
|
-
return nil if version_string.nil? || version_string.empty?
|
|
450
|
-
|
|
451
|
-
Gem::Version.new(version_string)
|
|
452
|
-
rescue ArgumentError
|
|
453
|
-
nil
|
|
487
|
+
self.class.send(:extract_version, result)
|
|
454
488
|
end
|
|
455
489
|
|
|
456
490
|
def parse_jsonl_output(output)
|
|
@@ -806,6 +840,28 @@ module AgentHarness
|
|
|
806
840
|
def hash_key_present?(value, key)
|
|
807
841
|
value.is_a?(Hash) && value.key?(key)
|
|
808
842
|
end
|
|
843
|
+
|
|
844
|
+
def resolve_chat_api_key
|
|
845
|
+
key = ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"] || read_copilot_cli_access_token
|
|
846
|
+
|
|
847
|
+
if key.nil? || key.strip.empty?
|
|
848
|
+
raise AuthenticationError.new(
|
|
849
|
+
"Chat mode requires a GitHub token. Set GITHUB_TOKEN or GH_TOKEN, or authenticate the Copilot CLI.",
|
|
850
|
+
provider: :github_copilot
|
|
851
|
+
)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
key.strip
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def read_copilot_cli_access_token
|
|
858
|
+
path = Pathname.new(File.join(Dir.home, ".copilot-cli-access-token"))
|
|
859
|
+
return nil unless path.file?
|
|
860
|
+
|
|
861
|
+
path.read
|
|
862
|
+
rescue Errno::ENOENT, Errno::EACCES, IOError
|
|
863
|
+
nil
|
|
864
|
+
end
|
|
809
865
|
end
|
|
810
866
|
end
|
|
811
867
|
end
|